light-plan 0.0.0-stage → 0.1.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 (963) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3181 -2
  3. package/assets/README.md +111 -0
  4. package/assets/agents/lpm-developer.md +468 -0
  5. package/assets/agents/lpm-planner.md +532 -0
  6. package/assets/harnesses/claude.yml +43 -0
  7. package/assets/harnesses/copilot.yml +54 -0
  8. package/assets/harnesses/reasonix.yml +49 -0
  9. package/assets/hcm/light-plan.yml +45 -0
  10. package/assets/skills/lpm-board-health.md +243 -0
  11. package/assets/skills/lpm-board-setup.md +354 -0
  12. package/assets/skills/lpm-delivery.md +344 -0
  13. package/assets/skills/lpm-planning.md +411 -0
  14. package/assets/skills/lpm-templates.md +144 -0
  15. package/assets/skills/lpm.md +267 -0
  16. package/dist/cli/agent-view.d.ts +27 -0
  17. package/dist/cli/agent-view.js +247 -0
  18. package/dist/cli/agent-view.js.map +1 -0
  19. package/dist/cli/commands/agent/assets.d.ts +64 -0
  20. package/dist/cli/commands/agent/assets.js +106 -0
  21. package/dist/cli/commands/agent/assets.js.map +1 -0
  22. package/dist/cli/commands/agent/glob.d.ts +25 -0
  23. package/dist/cli/commands/agent/glob.js +82 -0
  24. package/dist/cli/commands/agent/glob.js.map +1 -0
  25. package/dist/cli/commands/agent/index.d.ts +11 -0
  26. package/dist/cli/commands/agent/index.js +284 -0
  27. package/dist/cli/commands/agent/index.js.map +1 -0
  28. package/dist/cli/commands/agent/install.d.ts +59 -0
  29. package/dist/cli/commands/agent/install.js +131 -0
  30. package/dist/cli/commands/agent/install.js.map +1 -0
  31. package/dist/cli/commands/agent/mapping.d.ts +175 -0
  32. package/dist/cli/commands/agent/mapping.js +256 -0
  33. package/dist/cli/commands/agent/mapping.js.map +1 -0
  34. package/dist/cli/commands/check.d.ts +2 -0
  35. package/dist/cli/commands/check.js +81 -0
  36. package/dist/cli/commands/check.js.map +1 -0
  37. package/dist/cli/commands/comment.d.ts +2 -0
  38. package/dist/cli/commands/comment.js +104 -0
  39. package/dist/cli/commands/comment.js.map +1 -0
  40. package/dist/cli/commands/convert.d.ts +2 -0
  41. package/dist/cli/commands/convert.js +115 -0
  42. package/dist/cli/commands/convert.js.map +1 -0
  43. package/dist/cli/commands/copy.d.ts +2 -0
  44. package/dist/cli/commands/copy.js +69 -0
  45. package/dist/cli/commands/copy.js.map +1 -0
  46. package/dist/cli/commands/export.d.ts +11 -0
  47. package/dist/cli/commands/export.js +226 -0
  48. package/dist/cli/commands/export.js.map +1 -0
  49. package/dist/cli/commands/flag.d.ts +2 -0
  50. package/dist/cli/commands/flag.js +172 -0
  51. package/dist/cli/commands/flag.js.map +1 -0
  52. package/dist/cli/commands/git.d.ts +11 -0
  53. package/dist/cli/commands/git.js +357 -0
  54. package/dist/cli/commands/git.js.map +1 -0
  55. package/dist/cli/commands/hcm/bundle.d.ts +60 -0
  56. package/dist/cli/commands/hcm/bundle.js +166 -0
  57. package/dist/cli/commands/hcm/bundle.js.map +1 -0
  58. package/dist/cli/commands/hcm/index.d.ts +11 -0
  59. package/dist/cli/commands/hcm/index.js +162 -0
  60. package/dist/cli/commands/hcm/index.js.map +1 -0
  61. package/dist/cli/commands/init.d.ts +2 -0
  62. package/dist/cli/commands/init.js +81 -0
  63. package/dist/cli/commands/init.js.map +1 -0
  64. package/dist/cli/commands/insert.d.ts +2 -0
  65. package/dist/cli/commands/insert.js +70 -0
  66. package/dist/cli/commands/insert.js.map +1 -0
  67. package/dist/cli/commands/instructions.d.ts +7 -0
  68. package/dist/cli/commands/instructions.js +256 -0
  69. package/dist/cli/commands/instructions.js.map +1 -0
  70. package/dist/cli/commands/link.d.ts +2 -0
  71. package/dist/cli/commands/link.js +121 -0
  72. package/dist/cli/commands/link.js.map +1 -0
  73. package/dist/cli/commands/mcp/config.d.ts +71 -0
  74. package/dist/cli/commands/mcp/config.js +119 -0
  75. package/dist/cli/commands/mcp/config.js.map +1 -0
  76. package/dist/cli/commands/mcp/index.d.ts +9 -0
  77. package/dist/cli/commands/mcp/index.js +92 -0
  78. package/dist/cli/commands/mcp/index.js.map +1 -0
  79. package/dist/cli/commands/mcp/serve.d.ts +1 -0
  80. package/dist/cli/commands/mcp/serve.js +76 -0
  81. package/dist/cli/commands/mcp/serve.js.map +1 -0
  82. package/dist/cli/commands/mcp/setup.d.ts +1 -0
  83. package/dist/cli/commands/mcp/setup.js +83 -0
  84. package/dist/cli/commands/mcp/setup.js.map +1 -0
  85. package/dist/cli/commands/me.d.ts +2 -0
  86. package/dist/cli/commands/me.js +85 -0
  87. package/dist/cli/commands/me.js.map +1 -0
  88. package/dist/cli/commands/move.d.ts +2 -0
  89. package/dist/cli/commands/move.js +81 -0
  90. package/dist/cli/commands/move.js.map +1 -0
  91. package/dist/cli/commands/new.d.ts +2 -0
  92. package/dist/cli/commands/new.js +222 -0
  93. package/dist/cli/commands/new.js.map +1 -0
  94. package/dist/cli/commands/open.d.ts +2 -0
  95. package/dist/cli/commands/open.js +43 -0
  96. package/dist/cli/commands/open.js.map +1 -0
  97. package/dist/cli/commands/period.d.ts +10 -0
  98. package/dist/cli/commands/period.js +165 -0
  99. package/dist/cli/commands/period.js.map +1 -0
  100. package/dist/cli/commands/profile.d.ts +2 -0
  101. package/dist/cli/commands/profile.js +133 -0
  102. package/dist/cli/commands/profile.js.map +1 -0
  103. package/dist/cli/commands/queue.d.ts +2 -0
  104. package/dist/cli/commands/queue.js +582 -0
  105. package/dist/cli/commands/queue.js.map +1 -0
  106. package/dist/cli/commands/remote.d.ts +10 -0
  107. package/dist/cli/commands/remote.js +3142 -0
  108. package/dist/cli/commands/remote.js.map +1 -0
  109. package/dist/cli/commands/rm.d.ts +2 -0
  110. package/dist/cli/commands/rm.js +63 -0
  111. package/dist/cli/commands/rm.js.map +1 -0
  112. package/dist/cli/commands/set.d.ts +2 -0
  113. package/dist/cli/commands/set.js +139 -0
  114. package/dist/cli/commands/set.js.map +1 -0
  115. package/dist/cli/commands/split.d.ts +2 -0
  116. package/dist/cli/commands/split.js +90 -0
  117. package/dist/cli/commands/split.js.map +1 -0
  118. package/dist/cli/commands/task.d.ts +2 -0
  119. package/dist/cli/commands/task.js +298 -0
  120. package/dist/cli/commands/task.js.map +1 -0
  121. package/dist/cli/commands/team.d.ts +2 -0
  122. package/dist/cli/commands/team.js +114 -0
  123. package/dist/cli/commands/team.js.map +1 -0
  124. package/dist/cli/commands/template.d.ts +2 -0
  125. package/dist/cli/commands/template.js +338 -0
  126. package/dist/cli/commands/template.js.map +1 -0
  127. package/dist/cli/commands/ui.d.ts +2 -0
  128. package/dist/cli/commands/ui.js +98 -0
  129. package/dist/cli/commands/ui.js.map +1 -0
  130. package/dist/cli/commands/upstream.d.ts +2 -0
  131. package/dist/cli/commands/upstream.js +218 -0
  132. package/dist/cli/commands/upstream.js.map +1 -0
  133. package/dist/cli/context.d.ts +24 -0
  134. package/dist/cli/context.js +52 -0
  135. package/dist/cli/context.js.map +1 -0
  136. package/dist/cli/index.d.ts +2 -0
  137. package/dist/cli/index.js +208 -0
  138. package/dist/cli/index.js.map +1 -0
  139. package/dist/cli/live.d.ts +71 -0
  140. package/dist/cli/live.js +182 -0
  141. package/dist/cli/live.js.map +1 -0
  142. package/dist/cli/plan.d.ts +28 -0
  143. package/dist/cli/plan.js +76 -0
  144. package/dist/cli/plan.js.map +1 -0
  145. package/dist/cli/prompt.d.ts +60 -0
  146. package/dist/cli/prompt.js +185 -0
  147. package/dist/cli/prompt.js.map +1 -0
  148. package/dist/cli/ui.d.ts +65 -0
  149. package/dist/cli/ui.js +79 -0
  150. package/dist/cli/ui.js.map +1 -0
  151. package/dist/core/board/dependency-rollup.d.ts +54 -0
  152. package/dist/core/board/dependency-rollup.js +72 -0
  153. package/dist/core/board/dependency-rollup.js.map +1 -0
  154. package/dist/core/board/flag-rollup.d.ts +33 -0
  155. package/dist/core/board/flag-rollup.js +56 -0
  156. package/dist/core/board/flag-rollup.js.map +1 -0
  157. package/dist/core/board/index.d.ts +21 -0
  158. package/dist/core/board/index.js +22 -0
  159. package/dist/core/board/index.js.map +1 -0
  160. package/dist/core/board/load/cache.d.ts +52 -0
  161. package/dist/core/board/load/cache.js +105 -0
  162. package/dist/core/board/load/cache.js.map +1 -0
  163. package/dist/core/board/load/fields.d.ts +33 -0
  164. package/dist/core/board/load/fields.js +154 -0
  165. package/dist/core/board/load/fields.js.map +1 -0
  166. package/dist/core/board/load/scan.d.ts +39 -0
  167. package/dist/core/board/load/scan.js +88 -0
  168. package/dist/core/board/load/scan.js.map +1 -0
  169. package/dist/core/board/load/tree.d.ts +10 -0
  170. package/dist/core/board/load/tree.js +114 -0
  171. package/dist/core/board/load/tree.js.map +1 -0
  172. package/dist/core/board/load.d.ts +69 -0
  173. package/dist/core/board/load.js +204 -0
  174. package/dist/core/board/load.js.map +1 -0
  175. package/dist/core/board/query.d.ts +126 -0
  176. package/dist/core/board/query.js +291 -0
  177. package/dist/core/board/query.js.map +1 -0
  178. package/dist/core/board/registry.d.ts +51 -0
  179. package/dist/core/board/registry.js +98 -0
  180. package/dist/core/board/registry.js.map +1 -0
  181. package/dist/core/board/remote-scopes.d.ts +71 -0
  182. package/dist/core/board/remote-scopes.js +103 -0
  183. package/dist/core/board/remote-scopes.js.map +1 -0
  184. package/dist/core/board/rollup.d.ts +32 -0
  185. package/dist/core/board/rollup.js +67 -0
  186. package/dist/core/board/rollup.js.map +1 -0
  187. package/dist/core/board/scope.d.ts +81 -0
  188. package/dist/core/board/scope.js +134 -0
  189. package/dist/core/board/scope.js.map +1 -0
  190. package/dist/core/board/simulate.d.ts +138 -0
  191. package/dist/core/board/simulate.js +141 -0
  192. package/dist/core/board/simulate.js.map +1 -0
  193. package/dist/core/board/tasks/index.d.ts +25 -0
  194. package/dist/core/board/tasks/index.js +26 -0
  195. package/dist/core/board/tasks/index.js.map +1 -0
  196. package/dist/core/board/tasks/ranking.d.ts +155 -0
  197. package/dist/core/board/tasks/ranking.js +374 -0
  198. package/dist/core/board/tasks/ranking.js.map +1 -0
  199. package/dist/core/board/tasks/roster.d.ts +42 -0
  200. package/dist/core/board/tasks/roster.js +82 -0
  201. package/dist/core/board/tasks/roster.js.map +1 -0
  202. package/dist/core/board/tasks.d.ts +1 -0
  203. package/dist/core/board/tasks.js +2 -0
  204. package/dist/core/board/tasks.js.map +1 -0
  205. package/dist/core/config/index.d.ts +9 -0
  206. package/dist/core/config/index.js +10 -0
  207. package/dist/core/config/index.js.map +1 -0
  208. package/dist/core/config/lookup.d.ts +114 -0
  209. package/dist/core/config/lookup.js +221 -0
  210. package/dist/core/config/lookup.js.map +1 -0
  211. package/dist/core/config/remote-blocks.d.ts +17 -0
  212. package/dist/core/config/remote-blocks.js +47 -0
  213. package/dist/core/config/remote-blocks.js.map +1 -0
  214. package/dist/core/config/schema.d.ts +10 -0
  215. package/dist/core/config/schema.js +451 -0
  216. package/dist/core/config/schema.js.map +1 -0
  217. package/dist/core/errors.d.ts +23 -0
  218. package/dist/core/errors.js +31 -0
  219. package/dist/core/errors.js.map +1 -0
  220. package/dist/core/gitsync/hosts.d.ts +40 -0
  221. package/dist/core/gitsync/hosts.js +115 -0
  222. package/dist/core/gitsync/hosts.js.map +1 -0
  223. package/dist/core/gitsync/index.d.ts +25 -0
  224. package/dist/core/gitsync/index.js +25 -0
  225. package/dist/core/gitsync/index.js.map +1 -0
  226. package/dist/core/gitsync/integrate.d.ts +72 -0
  227. package/dist/core/gitsync/integrate.js +115 -0
  228. package/dist/core/gitsync/integrate.js.map +1 -0
  229. package/dist/core/gitsync/repo.d.ts +61 -0
  230. package/dist/core/gitsync/repo.js +213 -0
  231. package/dist/core/gitsync/repo.js.map +1 -0
  232. package/dist/core/gitsync/run.d.ts +58 -0
  233. package/dist/core/gitsync/run.js +102 -0
  234. package/dist/core/gitsync/run.js.map +1 -0
  235. package/dist/core/gitsync/status.d.ts +59 -0
  236. package/dist/core/gitsync/status.js +81 -0
  237. package/dist/core/gitsync/status.js.map +1 -0
  238. package/dist/core/gitsync/sync.d.ts +111 -0
  239. package/dist/core/gitsync/sync.js +275 -0
  240. package/dist/core/gitsync/sync.js.map +1 -0
  241. package/dist/core/index.d.ts +31 -0
  242. package/dist/core/index.js +32 -0
  243. package/dist/core/index.js.map +1 -0
  244. package/dist/core/instructions/analyze.d.ts +70 -0
  245. package/dist/core/instructions/analyze.js +275 -0
  246. package/dist/core/instructions/analyze.js.map +1 -0
  247. package/dist/core/instructions/builtin.d.ts +21 -0
  248. package/dist/core/instructions/builtin.js +133 -0
  249. package/dist/core/instructions/builtin.js.map +1 -0
  250. package/dist/core/instructions/context.d.ts +155 -0
  251. package/dist/core/instructions/context.js +209 -0
  252. package/dist/core/instructions/context.js.map +1 -0
  253. package/dist/core/instructions/index.d.ts +29 -0
  254. package/dist/core/instructions/index.js +30 -0
  255. package/dist/core/instructions/index.js.map +1 -0
  256. package/dist/core/instructions/instructions.d.ts +90 -0
  257. package/dist/core/instructions/instructions.js +179 -0
  258. package/dist/core/instructions/instructions.js.map +1 -0
  259. package/dist/core/instructions/template.d.ts +108 -0
  260. package/dist/core/instructions/template.js +252 -0
  261. package/dist/core/instructions/template.js.map +1 -0
  262. package/dist/core/model/attributes.d.ts +16 -0
  263. package/dist/core/model/attributes.js +119 -0
  264. package/dist/core/model/attributes.js.map +1 -0
  265. package/dist/core/model/index.d.ts +10 -0
  266. package/dist/core/model/index.js +11 -0
  267. package/dist/core/model/index.js.map +1 -0
  268. package/dist/core/model/links.d.ts +34 -0
  269. package/dist/core/model/links.js +114 -0
  270. package/dist/core/model/links.js.map +1 -0
  271. package/dist/core/model/profile.d.ts +31 -0
  272. package/dist/core/model/profile.js +8 -0
  273. package/dist/core/model/profile.js.map +1 -0
  274. package/dist/core/model/types.d.ts +441 -0
  275. package/dist/core/model/types.js +167 -0
  276. package/dist/core/model/types.js.map +1 -0
  277. package/dist/core/model/zod.d.ts +6 -0
  278. package/dist/core/model/zod.js +11 -0
  279. package/dist/core/model/zod.js.map +1 -0
  280. package/dist/core/operations/board-index.d.ts +45 -0
  281. package/dist/core/operations/board-index.js +147 -0
  282. package/dist/core/operations/board-index.js.map +1 -0
  283. package/dist/core/operations/claim.d.ts +69 -0
  284. package/dist/core/operations/claim.js +115 -0
  285. package/dist/core/operations/claim.js.map +1 -0
  286. package/dist/core/operations/comment.d.ts +30 -0
  287. package/dist/core/operations/comment.js +46 -0
  288. package/dist/core/operations/comment.js.map +1 -0
  289. package/dist/core/operations/create.d.ts +82 -0
  290. package/dist/core/operations/create.js +306 -0
  291. package/dist/core/operations/create.js.map +1 -0
  292. package/dist/core/operations/flag.d.ts +58 -0
  293. package/dist/core/operations/flag.js +95 -0
  294. package/dist/core/operations/flag.js.map +1 -0
  295. package/dist/core/operations/git-sync.d.ts +148 -0
  296. package/dist/core/operations/git-sync.js +341 -0
  297. package/dist/core/operations/git-sync.js.map +1 -0
  298. package/dist/core/operations/index.d.ts +44 -0
  299. package/dist/core/operations/index.js +45 -0
  300. package/dist/core/operations/index.js.map +1 -0
  301. package/dist/core/operations/init.d.ts +25 -0
  302. package/dist/core/operations/init.js +122 -0
  303. package/dist/core/operations/init.js.map +1 -0
  304. package/dist/core/operations/link.d.ts +40 -0
  305. package/dist/core/operations/link.js +131 -0
  306. package/dist/core/operations/link.js.map +1 -0
  307. package/dist/core/operations/move.d.ts +43 -0
  308. package/dist/core/operations/move.js +103 -0
  309. package/dist/core/operations/move.js.map +1 -0
  310. package/dist/core/operations/profile.d.ts +23 -0
  311. package/dist/core/operations/profile.js +26 -0
  312. package/dist/core/operations/profile.js.map +1 -0
  313. package/dist/core/operations/remotes-off.d.ts +12 -0
  314. package/dist/core/operations/remotes-off.js +67 -0
  315. package/dist/core/operations/remotes-off.js.map +1 -0
  316. package/dist/core/operations/remove.d.ts +18 -0
  317. package/dist/core/operations/remove.js +114 -0
  318. package/dist/core/operations/remove.js.map +1 -0
  319. package/dist/core/operations/retype.d.ts +25 -0
  320. package/dist/core/operations/retype.js +59 -0
  321. package/dist/core/operations/retype.js.map +1 -0
  322. package/dist/core/operations/rollup.d.ts +74 -0
  323. package/dist/core/operations/rollup.js +121 -0
  324. package/dist/core/operations/rollup.js.map +1 -0
  325. package/dist/core/operations/shared.d.ts +133 -0
  326. package/dist/core/operations/shared.js +383 -0
  327. package/dist/core/operations/shared.js.map +1 -0
  328. package/dist/core/operations/update.d.ts +43 -0
  329. package/dist/core/operations/update.js +161 -0
  330. package/dist/core/operations/update.js.map +1 -0
  331. package/dist/core/operations/user.d.ts +27 -0
  332. package/dist/core/operations/user.js +68 -0
  333. package/dist/core/operations/user.js.map +1 -0
  334. package/dist/core/profile/current.d.ts +35 -0
  335. package/dist/core/profile/current.js +31 -0
  336. package/dist/core/profile/current.js.map +1 -0
  337. package/dist/core/profile/index.d.ts +14 -0
  338. package/dist/core/profile/index.js +15 -0
  339. package/dist/core/profile/index.js.map +1 -0
  340. package/dist/core/profile/schema.d.ts +9 -0
  341. package/dist/core/profile/schema.js +100 -0
  342. package/dist/core/profile/schema.js.map +1 -0
  343. package/dist/core/storage/activity.d.ts +78 -0
  344. package/dist/core/storage/activity.js +131 -0
  345. package/dist/core/storage/activity.js.map +1 -0
  346. package/dist/core/storage/atomic.d.ts +34 -0
  347. package/dist/core/storage/atomic.js +117 -0
  348. package/dist/core/storage/atomic.js.map +1 -0
  349. package/dist/core/storage/comments.d.ts +39 -0
  350. package/dist/core/storage/comments.js +88 -0
  351. package/dist/core/storage/comments.js.map +1 -0
  352. package/dist/core/storage/document.d.ts +16 -0
  353. package/dist/core/storage/document.js +98 -0
  354. package/dist/core/storage/document.js.map +1 -0
  355. package/dist/core/storage/frontmatter.d.ts +16 -0
  356. package/dist/core/storage/frontmatter.js +33 -0
  357. package/dist/core/storage/frontmatter.js.map +1 -0
  358. package/dist/core/storage/git.d.ts +8 -0
  359. package/dist/core/storage/git.js +48 -0
  360. package/dist/core/storage/git.js.map +1 -0
  361. package/dist/core/storage/index.d.ts +27 -0
  362. package/dist/core/storage/index.js +28 -0
  363. package/dist/core/storage/index.js.map +1 -0
  364. package/dist/core/storage/local.d.ts +57 -0
  365. package/dist/core/storage/local.js +133 -0
  366. package/dist/core/storage/local.js.map +1 -0
  367. package/dist/core/storage/lock.d.ts +85 -0
  368. package/dist/core/storage/lock.js +364 -0
  369. package/dist/core/storage/lock.js.map +1 -0
  370. package/dist/core/storage/paths.d.ts +125 -0
  371. package/dist/core/storage/paths.js +202 -0
  372. package/dist/core/storage/paths.js.map +1 -0
  373. package/dist/core/storage/state.d.ts +32 -0
  374. package/dist/core/storage/state.js +71 -0
  375. package/dist/core/storage/state.js.map +1 -0
  376. package/dist/core/storage/templates.d.ts +29 -0
  377. package/dist/core/storage/templates.js +62 -0
  378. package/dist/core/storage/templates.js.map +1 -0
  379. package/dist/core/storage/views.d.ts +18 -0
  380. package/dist/core/storage/views.js +62 -0
  381. package/dist/core/storage/views.js.map +1 -0
  382. package/dist/core/validation/check.d.ts +4 -0
  383. package/dist/core/validation/check.js +22 -0
  384. package/dist/core/validation/check.js.map +1 -0
  385. package/dist/core/validation/checks/collection.d.ts +4 -0
  386. package/dist/core/validation/checks/collection.js +152 -0
  387. package/dist/core/validation/checks/collection.js.map +1 -0
  388. package/dist/core/validation/checks/counters.d.ts +3 -0
  389. package/dist/core/validation/checks/counters.js +23 -0
  390. package/dist/core/validation/checks/counters.js.map +1 -0
  391. package/dist/core/validation/checks/dependencies.d.ts +3 -0
  392. package/dist/core/validation/checks/dependencies.js +58 -0
  393. package/dist/core/validation/checks/dependencies.js.map +1 -0
  394. package/dist/core/validation/checks/gitignore.d.ts +10 -0
  395. package/dist/core/validation/checks/gitignore.js +22 -0
  396. package/dist/core/validation/checks/gitignore.js.map +1 -0
  397. package/dist/core/validation/checks/index-file.d.ts +8 -0
  398. package/dist/core/validation/checks/index-file.js +26 -0
  399. package/dist/core/validation/checks/index-file.js.map +1 -0
  400. package/dist/core/validation/checks/index.d.ts +20 -0
  401. package/dist/core/validation/checks/index.js +21 -0
  402. package/dist/core/validation/checks/index.js.map +1 -0
  403. package/dist/core/validation/checks/issues.d.ts +3 -0
  404. package/dist/core/validation/checks/issues.js +152 -0
  405. package/dist/core/validation/checks/issues.js.map +1 -0
  406. package/dist/core/validation/checks/periods.d.ts +3 -0
  407. package/dist/core/validation/checks/periods.js +89 -0
  408. package/dist/core/validation/checks/periods.js.map +1 -0
  409. package/dist/core/validation/checks/remotes.d.ts +20 -0
  410. package/dist/core/validation/checks/remotes.js +42 -0
  411. package/dist/core/validation/checks/remotes.js.map +1 -0
  412. package/dist/core/validation/checks/resources.d.ts +3 -0
  413. package/dist/core/validation/checks/resources.js +77 -0
  414. package/dist/core/validation/checks/resources.js.map +1 -0
  415. package/dist/core/validation/checks/rollup.d.ts +27 -0
  416. package/dist/core/validation/checks/rollup.js +53 -0
  417. package/dist/core/validation/checks/rollup.js.map +1 -0
  418. package/dist/core/validation/checks/squads.d.ts +3 -0
  419. package/dist/core/validation/checks/squads.js +42 -0
  420. package/dist/core/validation/checks/squads.js.map +1 -0
  421. package/dist/core/validation/checks/templates.d.ts +14 -0
  422. package/dist/core/validation/checks/templates.js +119 -0
  423. package/dist/core/validation/checks/templates.js.map +1 -0
  424. package/dist/core/validation/fix.d.ts +7 -0
  425. package/dist/core/validation/fix.js +273 -0
  426. package/dist/core/validation/fix.js.map +1 -0
  427. package/dist/core/validation/index.d.ts +7 -0
  428. package/dist/core/validation/index.js +8 -0
  429. package/dist/core/validation/index.js.map +1 -0
  430. package/dist/core/validation/shared.d.ts +47 -0
  431. package/dist/core/validation/shared.js +66 -0
  432. package/dist/core/validation/shared.js.map +1 -0
  433. package/dist/mcp/context.d.ts +70 -0
  434. package/dist/mcp/context.js +110 -0
  435. package/dist/mcp/context.js.map +1 -0
  436. package/dist/mcp/index.d.ts +49 -0
  437. package/dist/mcp/index.js +90 -0
  438. package/dist/mcp/index.js.map +1 -0
  439. package/dist/mcp/reply.d.ts +28 -0
  440. package/dist/mcp/reply.js +34 -0
  441. package/dist/mcp/reply.js.map +1 -0
  442. package/dist/mcp/tools/plan/create.d.ts +3 -0
  443. package/dist/mcp/tools/plan/create.js +128 -0
  444. package/dist/mcp/tools/plan/create.js.map +1 -0
  445. package/dist/mcp/tools/plan/delete.d.ts +3 -0
  446. package/dist/mcp/tools/plan/delete.js +35 -0
  447. package/dist/mcp/tools/plan/delete.js.map +1 -0
  448. package/dist/mcp/tools/plan/graph.d.ts +3 -0
  449. package/dist/mcp/tools/plan/graph.js +53 -0
  450. package/dist/mcp/tools/plan/graph.js.map +1 -0
  451. package/dist/mcp/tools/plan/index.d.ts +15 -0
  452. package/dist/mcp/tools/plan/index.js +16 -0
  453. package/dist/mcp/tools/plan/index.js.map +1 -0
  454. package/dist/mcp/tools/plan/link.d.ts +3 -0
  455. package/dist/mcp/tools/plan/link.js +46 -0
  456. package/dist/mcp/tools/plan/link.js.map +1 -0
  457. package/dist/mcp/tools/plan/registrar.d.ts +10 -0
  458. package/dist/mcp/tools/plan/registrar.js +24 -0
  459. package/dist/mcp/tools/plan/registrar.js.map +1 -0
  460. package/dist/mcp/tools/plan/reshape.d.ts +3 -0
  461. package/dist/mcp/tools/plan/reshape.js +95 -0
  462. package/dist/mcp/tools/plan/reshape.js.map +1 -0
  463. package/dist/mcp/tools/plan/timeline.d.ts +3 -0
  464. package/dist/mcp/tools/plan/timeline.js +68 -0
  465. package/dist/mcp/tools/plan/timeline.js.map +1 -0
  466. package/dist/mcp/tools/plan/upstream.d.ts +14 -0
  467. package/dist/mcp/tools/plan/upstream.js +69 -0
  468. package/dist/mcp/tools/plan/upstream.js.map +1 -0
  469. package/dist/mcp/tools/plan.d.ts +1 -0
  470. package/dist/mcp/tools/plan.js +2 -0
  471. package/dist/mcp/tools/plan.js.map +1 -0
  472. package/dist/mcp/tools/read.d.ts +3 -0
  473. package/dist/mcp/tools/read.js +339 -0
  474. package/dist/mcp/tools/read.js.map +1 -0
  475. package/dist/mcp/tools/remote.d.ts +3 -0
  476. package/dist/mcp/tools/remote.js +254 -0
  477. package/dist/mcp/tools/remote.js.map +1 -0
  478. package/dist/mcp/tools/templates.d.ts +12 -0
  479. package/dist/mcp/tools/templates.js +165 -0
  480. package/dist/mcp/tools/templates.js.map +1 -0
  481. package/dist/mcp/tools/work.d.ts +16 -0
  482. package/dist/mcp/tools/work.js +359 -0
  483. package/dist/mcp/tools/work.js.map +1 -0
  484. package/dist/remote/accounts.d.ts +158 -0
  485. package/dist/remote/accounts.js +207 -0
  486. package/dist/remote/accounts.js.map +1 -0
  487. package/dist/remote/adopt.d.ts +139 -0
  488. package/dist/remote/adopt.js +181 -0
  489. package/dist/remote/adopt.js.map +1 -0
  490. package/dist/remote/anchor.d.ts +35 -0
  491. package/dist/remote/anchor.js +53 -0
  492. package/dist/remote/anchor.js.map +1 -0
  493. package/dist/remote/attributes.d.ts +103 -0
  494. package/dist/remote/attributes.js +245 -0
  495. package/dist/remote/attributes.js.map +1 -0
  496. package/dist/remote/audit.d.ts +112 -0
  497. package/dist/remote/audit.js +215 -0
  498. package/dist/remote/audit.js.map +1 -0
  499. package/dist/remote/capabilities.d.ts +267 -0
  500. package/dist/remote/capabilities.js +225 -0
  501. package/dist/remote/capabilities.js.map +1 -0
  502. package/dist/remote/check.d.ts +49 -0
  503. package/dist/remote/check.js +356 -0
  504. package/dist/remote/check.js.map +1 -0
  505. package/dist/remote/comments.d.ts +86 -0
  506. package/dist/remote/comments.js +87 -0
  507. package/dist/remote/comments.js.map +1 -0
  508. package/dist/remote/config-file.d.ts +237 -0
  509. package/dist/remote/config-file.js +640 -0
  510. package/dist/remote/config-file.js.map +1 -0
  511. package/dist/remote/conflicts.d.ts +110 -0
  512. package/dist/remote/conflicts.js +188 -0
  513. package/dist/remote/conflicts.js.map +1 -0
  514. package/dist/remote/connection-catalogue.d.ts +65 -0
  515. package/dist/remote/connection-catalogue.js +66 -0
  516. package/dist/remote/connection-catalogue.js.map +1 -0
  517. package/dist/remote/connection.d.ts +84 -0
  518. package/dist/remote/connection.js +229 -0
  519. package/dist/remote/connection.js.map +1 -0
  520. package/dist/remote/coverage.d.ts +65 -0
  521. package/dist/remote/coverage.js +244 -0
  522. package/dist/remote/coverage.js.map +1 -0
  523. package/dist/remote/credentials.d.ts +95 -0
  524. package/dist/remote/credentials.js +210 -0
  525. package/dist/remote/credentials.js.map +1 -0
  526. package/dist/remote/execute.d.ts +178 -0
  527. package/dist/remote/execute.js +1245 -0
  528. package/dist/remote/execute.js.map +1 -0
  529. package/dist/remote/fingerprint.d.ts +172 -0
  530. package/dist/remote/fingerprint.js +337 -0
  531. package/dist/remote/fingerprint.js.map +1 -0
  532. package/dist/remote/fixtures.d.ts +176 -0
  533. package/dist/remote/fixtures.js +432 -0
  534. package/dist/remote/fixtures.js.map +1 -0
  535. package/dist/remote/guard.d.ts +90 -0
  536. package/dist/remote/guard.js +83 -0
  537. package/dist/remote/guard.js.map +1 -0
  538. package/dist/remote/hierarchy.d.ts +141 -0
  539. package/dist/remote/hierarchy.js +129 -0
  540. package/dist/remote/hierarchy.js.map +1 -0
  541. package/dist/remote/index.d.ts +145 -0
  542. package/dist/remote/index.js +146 -0
  543. package/dist/remote/index.js.map +1 -0
  544. package/dist/remote/inspect.d.ts +103 -0
  545. package/dist/remote/inspect.js +152 -0
  546. package/dist/remote/inspect.js.map +1 -0
  547. package/dist/remote/labels.d.ts +75 -0
  548. package/dist/remote/labels.js +137 -0
  549. package/dist/remote/labels.js.map +1 -0
  550. package/dist/remote/ladder.d.ts +145 -0
  551. package/dist/remote/ladder.js +387 -0
  552. package/dist/remote/ladder.js.map +1 -0
  553. package/dist/remote/ledger.d.ts +97 -0
  554. package/dist/remote/ledger.js +148 -0
  555. package/dist/remote/ledger.js.map +1 -0
  556. package/dist/remote/lifecycle.d.ts +175 -0
  557. package/dist/remote/lifecycle.js +274 -0
  558. package/dist/remote/lifecycle.js.map +1 -0
  559. package/dist/remote/links.d.ts +411 -0
  560. package/dist/remote/links.js +748 -0
  561. package/dist/remote/links.js.map +1 -0
  562. package/dist/remote/managed-block.d.ts +182 -0
  563. package/dist/remote/managed-block.js +406 -0
  564. package/dist/remote/managed-block.js.map +1 -0
  565. package/dist/remote/managed-comment.d.ts +104 -0
  566. package/dist/remote/managed-comment.js +92 -0
  567. package/dist/remote/managed-comment.js.map +1 -0
  568. package/dist/remote/mapping.d.ts +282 -0
  569. package/dist/remote/mapping.js +336 -0
  570. package/dist/remote/mapping.js.map +1 -0
  571. package/dist/remote/merge.d.ts +172 -0
  572. package/dist/remote/merge.js +221 -0
  573. package/dist/remote/merge.js.map +1 -0
  574. package/dist/remote/periods.d.ts +209 -0
  575. package/dist/remote/periods.js +224 -0
  576. package/dist/remote/periods.js.map +1 -0
  577. package/dist/remote/plan.d.ts +671 -0
  578. package/dist/remote/plan.js +1210 -0
  579. package/dist/remote/plan.js.map +1 -0
  580. package/dist/remote/policy.d.ts +67 -0
  581. package/dist/remote/policy.js +67 -0
  582. package/dist/remote/policy.js.map +1 -0
  583. package/dist/remote/preflight.d.ts +112 -0
  584. package/dist/remote/preflight.js +342 -0
  585. package/dist/remote/preflight.js.map +1 -0
  586. package/dist/remote/prerequisites.d.ts +113 -0
  587. package/dist/remote/prerequisites.js +161 -0
  588. package/dist/remote/prerequisites.js.map +1 -0
  589. package/dist/remote/provider.d.ts +802 -0
  590. package/dist/remote/provider.js +27 -0
  591. package/dist/remote/provider.js.map +1 -0
  592. package/dist/remote/providers/github/config.d.ts +84 -0
  593. package/dist/remote/providers/github/config.js +161 -0
  594. package/dist/remote/providers/github/config.js.map +1 -0
  595. package/dist/remote/providers/github/connector.d.ts +33 -0
  596. package/dist/remote/providers/github/connector.js +774 -0
  597. package/dist/remote/providers/github/connector.js.map +1 -0
  598. package/dist/remote/providers/github/edges.d.ts +29 -0
  599. package/dist/remote/providers/github/edges.js +56 -0
  600. package/dist/remote/providers/github/edges.js.map +1 -0
  601. package/dist/remote/providers/github/hierarchy.d.ts +21 -0
  602. package/dist/remote/providers/github/hierarchy.js +46 -0
  603. package/dist/remote/providers/github/hierarchy.js.map +1 -0
  604. package/dist/remote/providers/github/index.d.ts +26 -0
  605. package/dist/remote/providers/github/index.js +69 -0
  606. package/dist/remote/providers/github/index.js.map +1 -0
  607. package/dist/remote/providers/github/project-iteration.d.ts +71 -0
  608. package/dist/remote/providers/github/project-iteration.js +203 -0
  609. package/dist/remote/providers/github/project-iteration.js.map +1 -0
  610. package/dist/remote/providers/github/project-provision.d.ts +130 -0
  611. package/dist/remote/providers/github/project-provision.js +247 -0
  612. package/dist/remote/providers/github/project-provision.js.map +1 -0
  613. package/dist/remote/providers/github/project-status.d.ts +115 -0
  614. package/dist/remote/providers/github/project-status.js +317 -0
  615. package/dist/remote/providers/github/project-status.js.map +1 -0
  616. package/dist/remote/providers/github/projects.d.ts +181 -0
  617. package/dist/remote/providers/github/projects.js +481 -0
  618. package/dist/remote/providers/github/projects.js.map +1 -0
  619. package/dist/remote/providers/github/translator.d.ts +103 -0
  620. package/dist/remote/providers/github/translator.js +400 -0
  621. package/dist/remote/providers/github/translator.js.map +1 -0
  622. package/dist/remote/providers/jira/config.d.ts +96 -0
  623. package/dist/remote/providers/jira/config.js +200 -0
  624. package/dist/remote/providers/jira/config.js.map +1 -0
  625. package/dist/remote/providers/jira/connector.d.ts +42 -0
  626. package/dist/remote/providers/jira/connector.js +1688 -0
  627. package/dist/remote/providers/jira/connector.js.map +1 -0
  628. package/dist/remote/providers/jira/fields.d.ts +292 -0
  629. package/dist/remote/providers/jira/fields.js +463 -0
  630. package/dist/remote/providers/jira/fields.js.map +1 -0
  631. package/dist/remote/providers/jira/hierarchy.d.ts +28 -0
  632. package/dist/remote/providers/jira/hierarchy.js +60 -0
  633. package/dist/remote/providers/jira/hierarchy.js.map +1 -0
  634. package/dist/remote/providers/jira/index.d.ts +29 -0
  635. package/dist/remote/providers/jira/index.js +100 -0
  636. package/dist/remote/providers/jira/index.js.map +1 -0
  637. package/dist/remote/providers/jira/links.d.ts +41 -0
  638. package/dist/remote/providers/jira/links.js +102 -0
  639. package/dist/remote/providers/jira/links.js.map +1 -0
  640. package/dist/remote/providers/jira/sprints.d.ts +80 -0
  641. package/dist/remote/providers/jira/sprints.js +107 -0
  642. package/dist/remote/providers/jira/sprints.js.map +1 -0
  643. package/dist/remote/providers/jira/translator.d.ts +61 -0
  644. package/dist/remote/providers/jira/translator.js +382 -0
  645. package/dist/remote/providers/jira/translator.js.map +1 -0
  646. package/dist/remote/providers/jira/types.d.ts +112 -0
  647. package/dist/remote/providers/jira/types.js +160 -0
  648. package/dist/remote/providers/jira/types.js.map +1 -0
  649. package/dist/remote/providers/jira/vocabulary.d.ts +55 -0
  650. package/dist/remote/providers/jira/vocabulary.js +91 -0
  651. package/dist/remote/providers/jira/vocabulary.js.map +1 -0
  652. package/dist/remote/providers/jsonfile/config.d.ts +61 -0
  653. package/dist/remote/providers/jsonfile/config.js +83 -0
  654. package/dist/remote/providers/jsonfile/config.js.map +1 -0
  655. package/dist/remote/providers/jsonfile/connector.d.ts +34 -0
  656. package/dist/remote/providers/jsonfile/connector.js +251 -0
  657. package/dist/remote/providers/jsonfile/connector.js.map +1 -0
  658. package/dist/remote/providers/jsonfile/index.d.ts +38 -0
  659. package/dist/remote/providers/jsonfile/index.js +90 -0
  660. package/dist/remote/providers/jsonfile/index.js.map +1 -0
  661. package/dist/remote/providers/jsonfile/store.d.ts +59 -0
  662. package/dist/remote/providers/jsonfile/store.js +48 -0
  663. package/dist/remote/providers/jsonfile/store.js.map +1 -0
  664. package/dist/remote/providers/jsonfile/translator.d.ts +54 -0
  665. package/dist/remote/providers/jsonfile/translator.js +199 -0
  666. package/dist/remote/providers/jsonfile/translator.js.map +1 -0
  667. package/dist/remote/providers/linear/config.d.ts +83 -0
  668. package/dist/remote/providers/linear/config.js +140 -0
  669. package/dist/remote/providers/linear/config.js.map +1 -0
  670. package/dist/remote/providers/linear/connector.d.ts +36 -0
  671. package/dist/remote/providers/linear/connector.js +438 -0
  672. package/dist/remote/providers/linear/connector.js.map +1 -0
  673. package/dist/remote/providers/linear/estimate.d.ts +63 -0
  674. package/dist/remote/providers/linear/estimate.js +112 -0
  675. package/dist/remote/providers/linear/estimate.js.map +1 -0
  676. package/dist/remote/providers/linear/index.d.ts +31 -0
  677. package/dist/remote/providers/linear/index.js +75 -0
  678. package/dist/remote/providers/linear/index.js.map +1 -0
  679. package/dist/remote/providers/linear/labels.d.ts +51 -0
  680. package/dist/remote/providers/linear/labels.js +129 -0
  681. package/dist/remote/providers/linear/labels.js.map +1 -0
  682. package/dist/remote/providers/linear/states.d.ts +97 -0
  683. package/dist/remote/providers/linear/states.js +124 -0
  684. package/dist/remote/providers/linear/states.js.map +1 -0
  685. package/dist/remote/providers/linear/translator.d.ts +48 -0
  686. package/dist/remote/providers/linear/translator.js +343 -0
  687. package/dist/remote/providers/linear/translator.js.map +1 -0
  688. package/dist/remote/providers/linear/vocabulary.d.ts +27 -0
  689. package/dist/remote/providers/linear/vocabulary.js +42 -0
  690. package/dist/remote/providers/linear/vocabulary.js.map +1 -0
  691. package/dist/remote/provision.d.ts +45 -0
  692. package/dist/remote/provision.js +81 -0
  693. package/dist/remote/provision.js.map +1 -0
  694. package/dist/remote/pull.d.ts +52 -0
  695. package/dist/remote/pull.js +116 -0
  696. package/dist/remote/pull.js.map +1 -0
  697. package/dist/remote/readiness.d.ts +67 -0
  698. package/dist/remote/readiness.js +336 -0
  699. package/dist/remote/readiness.js.map +1 -0
  700. package/dist/remote/rebase.d.ts +157 -0
  701. package/dist/remote/rebase.js +255 -0
  702. package/dist/remote/rebase.js.map +1 -0
  703. package/dist/remote/reconcile.d.ts +115 -0
  704. package/dist/remote/reconcile.js +140 -0
  705. package/dist/remote/reconcile.js.map +1 -0
  706. package/dist/remote/redact.d.ts +70 -0
  707. package/dist/remote/redact.js +132 -0
  708. package/dist/remote/redact.js.map +1 -0
  709. package/dist/remote/registry.d.ts +32 -0
  710. package/dist/remote/registry.js +54 -0
  711. package/dist/remote/registry.js.map +1 -0
  712. package/dist/remote/remotes.d.ts +94 -0
  713. package/dist/remote/remotes.js +182 -0
  714. package/dist/remote/remotes.js.map +1 -0
  715. package/dist/remote/render.d.ts +147 -0
  716. package/dist/remote/render.js +683 -0
  717. package/dist/remote/render.js.map +1 -0
  718. package/dist/remote/report.d.ts +63 -0
  719. package/dist/remote/report.js +345 -0
  720. package/dist/remote/report.js.map +1 -0
  721. package/dist/remote/resolutions.d.ts +115 -0
  722. package/dist/remote/resolutions.js +217 -0
  723. package/dist/remote/resolutions.js.map +1 -0
  724. package/dist/remote/scaffold.d.ts +134 -0
  725. package/dist/remote/scaffold.js +370 -0
  726. package/dist/remote/scaffold.js.map +1 -0
  727. package/dist/remote/scope.d.ts +57 -0
  728. package/dist/remote/scope.js +91 -0
  729. package/dist/remote/scope.js.map +1 -0
  730. package/dist/remote/selection.d.ts +38 -0
  731. package/dist/remote/selection.js +50 -0
  732. package/dist/remote/selection.js.map +1 -0
  733. package/dist/remote/shape.d.ts +141 -0
  734. package/dist/remote/shape.js +220 -0
  735. package/dist/remote/shape.js.map +1 -0
  736. package/dist/remote/status.d.ts +104 -0
  737. package/dist/remote/status.js +238 -0
  738. package/dist/remote/status.js.map +1 -0
  739. package/dist/remote/sync.d.ts +182 -0
  740. package/dist/remote/sync.js +523 -0
  741. package/dist/remote/sync.js.map +1 -0
  742. package/dist/remote/transport/budget.d.ts +48 -0
  743. package/dist/remote/transport/budget.js +68 -0
  744. package/dist/remote/transport/budget.js.map +1 -0
  745. package/dist/remote/transport/connector.d.ts +115 -0
  746. package/dist/remote/transport/connector.js +21 -0
  747. package/dist/remote/transport/connector.js.map +1 -0
  748. package/dist/remote/transport/error.d.ts +88 -0
  749. package/dist/remote/transport/error.js +196 -0
  750. package/dist/remote/transport/error.js.map +1 -0
  751. package/dist/remote/transport/gh.d.ts +20 -0
  752. package/dist/remote/transport/gh.js +41 -0
  753. package/dist/remote/transport/gh.js.map +1 -0
  754. package/dist/remote/transport/graphql.d.ts +25 -0
  755. package/dist/remote/transport/graphql.js +81 -0
  756. package/dist/remote/transport/graphql.js.map +1 -0
  757. package/dist/remote/transport/http.d.ts +47 -0
  758. package/dist/remote/transport/http.js +195 -0
  759. package/dist/remote/transport/http.js.map +1 -0
  760. package/dist/remote/transport/index.d.ts +18 -0
  761. package/dist/remote/transport/index.js +19 -0
  762. package/dist/remote/transport/index.js.map +1 -0
  763. package/dist/remote/transport/process.d.ts +39 -0
  764. package/dist/remote/transport/process.js +121 -0
  765. package/dist/remote/transport/process.js.map +1 -0
  766. package/dist/remote/transport/rest.d.ts +22 -0
  767. package/dist/remote/transport/rest.js +57 -0
  768. package/dist/remote/transport/rest.js.map +1 -0
  769. package/dist/remote/transport/retry.d.ts +91 -0
  770. package/dist/remote/transport/retry.js +138 -0
  771. package/dist/remote/transport/retry.js.map +1 -0
  772. package/dist/remote/transport-error.d.ts +19 -0
  773. package/dist/remote/transport-error.js +22 -0
  774. package/dist/remote/transport-error.js.map +1 -0
  775. package/dist/remote/vocabulary.d.ts +126 -0
  776. package/dist/remote/vocabulary.js +57 -0
  777. package/dist/remote/vocabulary.js.map +1 -0
  778. package/dist/runner/config.d.ts +7 -0
  779. package/dist/runner/config.js +61 -0
  780. package/dist/runner/config.js.map +1 -0
  781. package/dist/runner/git.d.ts +19 -0
  782. package/dist/runner/git.js +26 -0
  783. package/dist/runner/git.js.map +1 -0
  784. package/dist/runner/index.d.ts +20 -0
  785. package/dist/runner/index.js +21 -0
  786. package/dist/runner/index.js.map +1 -0
  787. package/dist/runner/loop.d.ts +46 -0
  788. package/dist/runner/loop.js +243 -0
  789. package/dist/runner/loop.js.map +1 -0
  790. package/dist/runner/pi.d.ts +39 -0
  791. package/dist/runner/pi.js +276 -0
  792. package/dist/runner/pi.js.map +1 -0
  793. package/dist/runner/shell.d.ts +44 -0
  794. package/dist/runner/shell.js +174 -0
  795. package/dist/runner/shell.js.map +1 -0
  796. package/dist/runner/stats.d.ts +19 -0
  797. package/dist/runner/stats.js +58 -0
  798. package/dist/runner/stats.js.map +1 -0
  799. package/dist/runner/types.d.ts +218 -0
  800. package/dist/runner/types.js +37 -0
  801. package/dist/runner/types.js.map +1 -0
  802. package/dist/server/git.d.ts +8 -0
  803. package/dist/server/git.js +50 -0
  804. package/dist/server/git.js.map +1 -0
  805. package/dist/server/http/respond.d.ts +13 -0
  806. package/dist/server/http/respond.js +59 -0
  807. package/dist/server/http/respond.js.map +1 -0
  808. package/dist/server/http/router.d.ts +27 -0
  809. package/dist/server/http/router.js +48 -0
  810. package/dist/server/http/router.js.map +1 -0
  811. package/dist/server/http/static.d.ts +14 -0
  812. package/dist/server/http/static.js +55 -0
  813. package/dist/server/http/static.js.map +1 -0
  814. package/dist/server/index.d.ts +58 -0
  815. package/dist/server/index.js +124 -0
  816. package/dist/server/index.js.map +1 -0
  817. package/dist/server/routes/board.d.ts +8 -0
  818. package/dist/server/routes/board.js +19 -0
  819. package/dist/server/routes/board.js.map +1 -0
  820. package/dist/server/routes/comments.d.ts +13 -0
  821. package/dist/server/routes/comments.js +36 -0
  822. package/dist/server/routes/comments.js.map +1 -0
  823. package/dist/server/routes/flags.d.ts +16 -0
  824. package/dist/server/routes/flags.js +37 -0
  825. package/dist/server/routes/flags.js.map +1 -0
  826. package/dist/server/routes/git.d.ts +13 -0
  827. package/dist/server/routes/git.js +84 -0
  828. package/dist/server/routes/git.js.map +1 -0
  829. package/dist/server/routes/remotes.d.ts +36 -0
  830. package/dist/server/routes/remotes.js +749 -0
  831. package/dist/server/routes/remotes.js.map +1 -0
  832. package/dist/server/routes/views.d.ts +10 -0
  833. package/dist/server/routes/views.js +63 -0
  834. package/dist/server/routes/views.js.map +1 -0
  835. package/dist/server/views/schema.d.ts +2 -0
  836. package/dist/server/views/schema.js +121 -0
  837. package/dist/server/views/schema.js.map +1 -0
  838. package/dist/server/views/store.d.ts +17 -0
  839. package/dist/server/views/store.js +67 -0
  840. package/dist/server/views/store.js.map +1 -0
  841. package/dist/shared/adf.d.ts +83 -0
  842. package/dist/shared/adf.js +795 -0
  843. package/dist/shared/adf.js.map +1 -0
  844. package/dist/shared/blocking.d.ts +130 -0
  845. package/dist/shared/blocking.js +179 -0
  846. package/dist/shared/blocking.js.map +1 -0
  847. package/dist/shared/changes.d.ts +100 -0
  848. package/dist/shared/changes.js +112 -0
  849. package/dist/shared/changes.js.map +1 -0
  850. package/dist/shared/cohesion.d.ts +75 -0
  851. package/dist/shared/cohesion.js +132 -0
  852. package/dist/shared/cohesion.js.map +1 -0
  853. package/dist/shared/dependency-rollup.d.ts +91 -0
  854. package/dist/shared/dependency-rollup.js +132 -0
  855. package/dist/shared/dependency-rollup.js.map +1 -0
  856. package/dist/shared/errors.d.ts +23 -0
  857. package/dist/shared/errors.js +24 -0
  858. package/dist/shared/errors.js.map +1 -0
  859. package/dist/shared/flag-rollup.d.ts +88 -0
  860. package/dist/shared/flag-rollup.js +119 -0
  861. package/dist/shared/flag-rollup.js.map +1 -0
  862. package/dist/shared/git-sync.d.ts +90 -0
  863. package/dist/shared/git-sync.js +9 -0
  864. package/dist/shared/git-sync.js.map +1 -0
  865. package/dist/shared/index.d.ts +40 -0
  866. package/dist/shared/index.js +41 -0
  867. package/dist/shared/index.js.map +1 -0
  868. package/dist/shared/model.d.ts +218 -0
  869. package/dist/shared/model.js +95 -0
  870. package/dist/shared/model.js.map +1 -0
  871. package/dist/shared/period-query.d.ts +21 -0
  872. package/dist/shared/period-query.js +26 -0
  873. package/dist/shared/period-query.js.map +1 -0
  874. package/dist/shared/period-stance.d.ts +45 -0
  875. package/dist/shared/period-stance.js +50 -0
  876. package/dist/shared/period-stance.js.map +1 -0
  877. package/dist/shared/plans/breakdown.d.ts +66 -0
  878. package/dist/shared/plans/breakdown.js +186 -0
  879. package/dist/shared/plans/breakdown.js.map +1 -0
  880. package/dist/shared/plans/index.d.ts +21 -0
  881. package/dist/shared/plans/index.js +22 -0
  882. package/dist/shared/plans/index.js.map +1 -0
  883. package/dist/shared/plans/instantiate.d.ts +61 -0
  884. package/dist/shared/plans/instantiate.js +184 -0
  885. package/dist/shared/plans/instantiate.js.map +1 -0
  886. package/dist/shared/plans/reading.d.ts +79 -0
  887. package/dist/shared/plans/reading.js +149 -0
  888. package/dist/shared/plans/reading.js.map +1 -0
  889. package/dist/shared/plans/reparent.d.ts +54 -0
  890. package/dist/shared/plans/reparent.js +217 -0
  891. package/dist/shared/plans/reparent.js.map +1 -0
  892. package/dist/shared/plans/timeline.d.ts +93 -0
  893. package/dist/shared/plans/timeline.js +224 -0
  894. package/dist/shared/plans/timeline.js.map +1 -0
  895. package/dist/shared/plans/upstream.d.ts +67 -0
  896. package/dist/shared/plans/upstream.js +73 -0
  897. package/dist/shared/plans/upstream.js.map +1 -0
  898. package/dist/shared/remote-api.d.ts +413 -0
  899. package/dist/shared/remote-api.js +11 -0
  900. package/dist/shared/remote-api.js.map +1 -0
  901. package/dist/shared/remote-coverage.d.ts +120 -0
  902. package/dist/shared/remote-coverage.js +98 -0
  903. package/dist/shared/remote-coverage.js.map +1 -0
  904. package/dist/shared/remote-readiness.d.ts +162 -0
  905. package/dist/shared/remote-readiness.js +49 -0
  906. package/dist/shared/remote-readiness.js.map +1 -0
  907. package/dist/shared/remote-status.d.ts +268 -0
  908. package/dist/shared/remote-status.js +79 -0
  909. package/dist/shared/remote-status.js.map +1 -0
  910. package/dist/shared/rollup.d.ts +90 -0
  911. package/dist/shared/rollup.js +119 -0
  912. package/dist/shared/rollup.js.map +1 -0
  913. package/dist/shared/static.d.ts +65 -0
  914. package/dist/shared/static.js +107 -0
  915. package/dist/shared/static.js.map +1 -0
  916. package/dist/shared/template-params.d.ts +80 -0
  917. package/dist/shared/template-params.js +141 -0
  918. package/dist/shared/template-params.js.map +1 -0
  919. package/dist/shared/view.d.ts +105 -0
  920. package/dist/shared/view.js +44 -0
  921. package/dist/shared/view.js.map +1 -0
  922. package/dist/shared/work-unit.d.ts +34 -0
  923. package/dist/shared/work-unit.js +47 -0
  924. package/dist/shared/work-unit.js.map +1 -0
  925. package/dist/sync/apply.d.ts +21 -0
  926. package/dist/sync/apply.js +149 -0
  927. package/dist/sync/apply.js.map +1 -0
  928. package/dist/sync/dto.d.ts +9 -0
  929. package/dist/sync/dto.js +162 -0
  930. package/dist/sync/dto.js.map +1 -0
  931. package/dist/sync/index.d.ts +24 -0
  932. package/dist/sync/index.js +25 -0
  933. package/dist/sync/index.js.map +1 -0
  934. package/dist/sync/patch.d.ts +13 -0
  935. package/dist/sync/patch.js +212 -0
  936. package/dist/sync/patch.js.map +1 -0
  937. package/dist/sync/session.d.ts +31 -0
  938. package/dist/sync/session.js +55 -0
  939. package/dist/sync/session.js.map +1 -0
  940. package/dist/sync/static.d.ts +11 -0
  941. package/dist/sync/static.js +33 -0
  942. package/dist/sync/static.js.map +1 -0
  943. package/package.json +95 -4
  944. package/templates/blank.yml +71 -0
  945. package/templates/context/bug.md +107 -0
  946. package/templates/context/default.md +119 -0
  947. package/templates/context/epic.md +81 -0
  948. package/templates/context/feature.md +91 -0
  949. package/templates/context/research.md +87 -0
  950. package/templates/context/review.md +81 -0
  951. package/templates/context/story.md +94 -0
  952. package/templates/context/sub_task.md +108 -0
  953. package/templates/context/task.md +86 -0
  954. package/templates/context/test.md +73 -0
  955. package/templates/context/user_story.md +147 -0
  956. package/templates/kanban.yml +226 -0
  957. package/templates/scrum.yml +567 -0
  958. package/web/dist/assets/index-D0j8OnHq.js +35 -0
  959. package/web/dist/assets/index-DFL7v8aP.css +1 -0
  960. package/web/dist/index.html +13 -0
  961. package/web/dist-viewer/assets/index-Bcns6OuC.css +1 -0
  962. package/web/dist-viewer/assets/index-vVXzeiFo.js +7 -0
  963. package/web/dist-viewer/index.html +14 -0
package/README.md CHANGED
@@ -1,3 +1,3182 @@
1
- # Temporary Holding Version
1
+ # Light Plan
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A lightweight, file-based issue tracker. Your Agile board lives in your repo as
4
+ folders and markdown files, versioned with git — no server, no database, no
5
+ account.
6
+
7
+ ```
8
+ .lpm/
9
+ ├── config.yml
10
+ ├── INDEX.md every id, title and document, nested (generated)
11
+ ├── board/ what gets built
12
+ │ └── LP-1/ program — Payments platform
13
+ │ ├── _issue.md
14
+ │ └── LP-2/ epic — Checkout revamp
15
+ │ ├── _issue.md
16
+ │ └── LP-3/ feature — Guest flow
17
+ │ ├── _issue.md
18
+ │ ├── LP-4/ user story — Guest checkout
19
+ │ └── LP-5/ bug — Cart total
20
+ ├── timeline/ when it gets built
21
+ │ └── TL-1/ product increment — 2026 H2
22
+ │ ├── _period.md
23
+ │ ├── TL-2/ sprint 1
24
+ │ └── TL-3/ sprint 2
25
+ ├── team/ who builds it
26
+ │ ├── RS-1/ a person — Alice Smith
27
+ │ │ └── _resource.md
28
+ │ └── RS-4/ a pool anyone can be drawn from — Jr. developer
29
+ │ └── _resource.md
30
+ └── templates/context/ what a developer is told to do it
31
+ ├── default.md
32
+ └── user_story.md
33
+ ```
34
+
35
+ Folder nesting *is* the hierarchy. Each folder is one document; its markdown
36
+ file holds YAML frontmatter (the structured fields) plus a body (the narrative).
37
+ Everything is plain text, so diffs, blame, branches, and pull requests work
38
+ exactly as they do for code.
39
+
40
+ A folder is named after its **id and nothing else**. Ids are short and never
41
+ change, so a path stays quotable, a title can be rewritten without moving
42
+ anything, and a board nested five levels deep does not run into the limits git
43
+ and Windows put on a path. The titles live in
44
+ [`.lpm/INDEX.md`](#indexmd-the-board-as-a-table-of-contents), which every
45
+ command that adds, removes, moves or renames a document rewrites for you.
46
+
47
+ There are three collections. `board/` holds **issues** — what gets built, nested
48
+ by scope. `timeline/` holds **periods** — sprints and increments, nested by
49
+ duration. `team/` holds **resources** — the people who do the work and the
50
+ generic pools work can wait in. They are independent: an issue at any level can
51
+ be scheduled into a period at any level via its `period:` field, and assigned to
52
+ any resource via its `assignee:` field.
53
+
54
+ `templates/context/` holds no documents. It is the one folder the engine never
55
+ walks: the layouts `lpm instructions` renders a [working brief](#working-briefs-the-context-to-actually-do-it)
56
+ with, so a team decides what a developer is handed when they pick an issue up.
57
+
58
+ ## Install
59
+
60
+ Requires Node 20+. The package is `light-plan` and the command it installs is
61
+ `lpm`:
62
+
63
+ ```bash
64
+ npm install -g light-plan
65
+ lpm init
66
+ ```
67
+
68
+ Or run it without installing anything:
69
+
70
+ ```bash
71
+ npx light-plan init
72
+ npx light-plan ui
73
+ ```
74
+
75
+ Spell it `npx light-plan`, never `npx lpm` — `lpm` on npm is an unrelated
76
+ package. `lpm mcp setup` and `lpm agent` notice when they were started through
77
+ npx and write a host config that starts the server with `npx -y light-plan mcp`,
78
+ so nothing points into npm's cache.
79
+
80
+ The experimental features — [`lpm queue agent`](#draining-the-queue-with-an-agent-lpm-queue-agent) and
81
+ [Jira sync](docs/remote-jira.md) — need packages a standard install leaves out;
82
+ their sections say what to add.
83
+
84
+ ### From a checkout
85
+
86
+ ```bash
87
+ make setup # installs, builds, and puts `lpm` on your PATH
88
+ ```
89
+
90
+ Requires GNU Make. Without Make:
91
+
92
+ ```bash
93
+ npm install
94
+ npm run build
95
+ npm link # puts `lpm` on your PATH
96
+ ```
97
+
98
+ `make doctor` checks your machine before you start, and `make` on its own lists
99
+ every target. See [Development](#development).
100
+
101
+ ## Quick start
102
+
103
+ ```bash
104
+ cd my-project
105
+ lpm init # creates .lpm/ from the scrum template
106
+
107
+ # What gets built
108
+ lpm new program -t "Payments platform" # -> LP-1
109
+ lpm new epic -t "Checkout revamp" -p LP-1 # -> LP-2
110
+ lpm new feature -t "Guest flow" -p LP-2 # -> LP-3
111
+ lpm new user_story -t "Payment gateway" -p LP-3 --set story_points=5
112
+ lpm new user_story -t "Guest checkout" -p LP-3 --set story_points=3
113
+
114
+ # When it gets built
115
+ lpm new increment -t "2026 H2" --starts 2026-07-01 --ends 2026-12-31 # -> TL-1
116
+ lpm new sprint -t "Sprint 1" --starts 2026-08-03 --ends 2026-08-14 -p TL-1
117
+
118
+ # Who builds it
119
+ lpm new person -t "Alice Smith" # -> RS-1
120
+ lpm new role -t "Jr. software developer" --capacity 3 # -> RS-2, a pool of three
121
+ lpm link RS-1 --covers RS-2 # Alice can pick up the pool's work
122
+
123
+ # Wire it together
124
+ lpm link LP-5 --depends-on LP-4 # LP-5 is blocked by LP-4
125
+ lpm move LP-4 --period TL-2 # schedule into Sprint 1
126
+ lpm move LP-4 --assignee RS-2 # park it in the junior pool
127
+
128
+ # Reuse what the team already worked out
129
+ lpm template list # the registry: reusable pieces of plan
130
+ lpm template apply TPL-3 --set name=Payments --under LP-4
131
+
132
+ # Work it
133
+ lpm me "Alice Smith" # this checkout is Alice's
134
+ lpm task next # what should I do?
135
+ lpm task start # claim it and start the clock
136
+ lpm task done
137
+
138
+ lpm queue simulate --user "Alice Smith" # her whole run, if she worked alone
139
+
140
+ lpm check # validate everything
141
+ ```
142
+
143
+ `lpm new <type>` decides what to create from the type name: issue types land in
144
+ `board/`, period types in `timeline/`, resource types in `team/`. A name can't
145
+ mean two of those — the config rejects that.
146
+
147
+ A generated issue:
148
+
149
+ ```markdown
150
+ ---
151
+ id: LP-5
152
+ type: user_story
153
+ title: Guest checkout
154
+ status: backlog
155
+ assignee: RS-1
156
+ period: TL-2
157
+ depends_on:
158
+ - LP-4
159
+ relates_to: []
160
+ related_files:
161
+ - docs/prd.md#L120-L164
162
+ - src/checkout/session.ts
163
+ created: 2026-07-31T15:02:27.576Z
164
+ updated: 2026-07-31T15:03:10.114Z
165
+ author: Jane Doe <jane@example.com>
166
+ story_points: 3
167
+ priority: medium
168
+ labels: []
169
+ ---
170
+
171
+ As a **<role>**, I want **<capability>**, so that **<benefit>**.
172
+
173
+ ## Acceptance Criteria
174
+
175
+ - [ ] **Given** <context> **when** <action> **then** <outcome>
176
+ ```
177
+
178
+ ## Dependencies
179
+
180
+ Two reserved fields on every issue:
181
+
182
+ | Field | Meaning |
183
+ | --- | --- |
184
+ | `depends_on` | Issues that block this one. Directional, cycle-checked. |
185
+ | `relates_to` | Non-blocking association. No ordering implied. |
186
+
187
+ ```bash
188
+ lpm link LP-7 --depends-on LP-3
189
+ lpm link LP-7 --depends-on LP-3,LP-4 --relates-to LP-9
190
+ lpm link LP-7 --depends-on LP-3 --remove
191
+ lpm new user_story -t "Checkout" -p LP-3 --depends-on LP-4
192
+ ```
193
+
194
+ `depends_on` is the only edge the engine acts on. There used to be a third,
195
+ `informed_by`, for the research an issue rested on — it gated the queue in
196
+ exactly the same way, which made it a second name for one relationship. It is
197
+ gone: why an issue is written the way it is belongs in its body and its
198
+ [related files](#related-files), and if work cannot start until a question is
199
+ answered, that is a dependency. A board still carrying the field is migrated by
200
+ `lpm check --fix`, which merges the ids into `depends_on`.
201
+
202
+ **Only the forward edge is stored.** The inverse — "what does this block?" — is
203
+ derived when the board loads and exposed as `board.dependents`. Storing both
204
+ sides would mean two files to keep in sync on every change, and a whole class of
205
+ reconciliation bugs for `check --fix` to chase.
206
+
207
+ **A dependency is inherited by everything inside the issue.** A story sits in a
208
+ feature, and a feature that waits on another feature waits on it *with
209
+ everything in it* — so you write the edge once, between the two features, and
210
+ the queue holds back every story under the second one. You do not have to wire
211
+ each story to each other story, and `lpm task next` will not hand somebody the
212
+ stories of a feature whose predecessor has not been started.
213
+
214
+ **A dependency on a container is cleared by the work inside it**, not by the
215
+ container's own status. Nobody moves a feature through the columns — the stories
216
+ under it are what get worked — so `LP-7 --depends-on LP-3` is satisfied once
217
+ every open piece of work under `LP-3` is finished, whatever column `LP-3` itself
218
+ is sitting in. Closing `LP-3` outright still answers for its contents, and
219
+ `lpm task next` names the dependency the way you wrote it (the feature, not the
220
+ five stories in it), because that is the document you would open to see where it
221
+ stands.
222
+
223
+ ```bash
224
+ lpm link LP-4 --depends-on LP-3 # feature LP-4 after feature LP-3
225
+ lpm task next # ...and no story under LP-4 is offered yet
226
+ lpm queue simulate # the whole sequence, in dependency order
227
+ ```
228
+
229
+ **A dependency is reflected onto the containers above it, up to the one they
230
+ share.** The inheritance above runs downward — an edge on a feature holds back
231
+ every story in it. This is the same fact read upward. A story in *Guest flow*
232
+ waiting on a story in *Sign-up* means *Guest flow* stands behind *Sign-up*, and
233
+ the two epics above them stand in the same order, and so on until the container
234
+ they both sit in, inside which there is nothing left to order. Write the edge
235
+ between the two pieces of work that actually have it; the levels above it are
236
+ read off the graph:
237
+
238
+ ```bash
239
+ lpm link LP-4 --depends-on LP-7
240
+ # Linked LP-4 Guest checkout
241
+ # depends on + LP-7
242
+ # also orders LP-3 Guest flow after LP-6 Sign-up
243
+ # also orders LP-2 Checkout after LP-5 Accounts
244
+
245
+ lpm upstream LP-3 # ...and the feature reads it back
246
+ ```
247
+
248
+ The reflection is **never written onto those containers**, and both halves of
249
+ that are deliberate. A `depends_on` on *Guest flow* would be inherited by every
250
+ story in it, so one story waiting on one story would hold back a dozen that are
251
+ waiting on nothing. And the reflection is not acyclic — two features that each
252
+ contain a story waiting on the other are an ordinary plan, and writing that down
253
+ would produce a loop `lpm link` has to refuse. So it is read, never gated: it
254
+ changes nothing about what the queue offers, and a loop in it is a fact about
255
+ the plan rather than a stall. It shows up in `lpm link`, in `lpm upstream`, in
256
+ the MCP `get_document` (`rolledUpBlockedBy` / `rolledUpBlocks`) and in the web
257
+ side panel, always naming the written dependency it comes from.
258
+
259
+ `lpm link` refuses an edge that would close a cycle, so bad state never reaches
260
+ the files. `lpm check` catches cycles introduced by hand-editing or by a merge,
261
+ along with references to issues that don't exist, self-references, and
262
+ duplicates. It also warns when a *done* issue is blocked by an unfinished one.
263
+ An edge pointing at one of the issue's own ancestors is ignored when work is
264
+ ranked rather than treated as a block — it could only stall the work on itself.
265
+
266
+ ## Related files
267
+
268
+ An issue is usually about some code. `related_files` says which:
269
+
270
+ ```bash
271
+ lpm new user_story -t "Guest checkout" -p LP-3 \
272
+ --related "docs/prd.md#L120-L164" --related src/checkout/session.ts
273
+
274
+ lpm set LP-5 --related src/checkout/pay.ts # attach another
275
+ lpm set LP-5 --unrelated src/checkout/session.ts # detach one
276
+ ```
277
+
278
+ Each entry is a path from the project root, optionally with a line range —
279
+ `docs/prd.md#L120-L164` points at the paragraphs of the PRD the story was written
280
+ from, `src/checkout/session.ts` at the module it will change. It is plain text
281
+ and **never checked against the filesystem**: an issue naming a file that does
282
+ not exist yet is usually the point of the issue, and a board that failed `check`
283
+ because somebody renamed a module would teach people to stop filling this in.
284
+
285
+ Two things read it. A working brief prints the issue's own files under *Files
286
+ this is about* — the first thing to open — and, for each issue this one was
287
+ sequenced after, the files that work touched. And the web app shows them in the
288
+ side panel, where they can be edited as one path per line.
289
+
290
+ That second part is what makes the field worth the typing. A story that says
291
+ "depends on LP-4" tells you the order; a story that says "depends on LP-4, which
292
+ touched `src/checkout/session.ts`, and I am about to change the same file" tells
293
+ you to go and read what LP-4 did first.
294
+
295
+ ## Time hierarchy
296
+
297
+ Periods answer "when does this get built?". They are configured exactly like
298
+ issue types — their own hierarchy, their own attributes, their own body
299
+ templates — and live under `.lpm/timeline` with real files carrying their own
300
+ metadata:
301
+
302
+ ```markdown
303
+ ---
304
+ id: TL-2
305
+ type: sprint
306
+ title: Sprint 1
307
+ starts: 2026-08-03
308
+ ends: 2026-08-14
309
+ created: 2026-07-31T16:52:14.421Z
310
+ author: Jane Doe <jane@example.com>
311
+ goal: Guests can pay without an account
312
+ capacity: 34
313
+ committed_points: 31
314
+ completed_points: null
315
+ ---
316
+
317
+ ## Sprint Goal
318
+
319
+ One sentence the team can rally behind.
320
+
321
+ ## Review
322
+ ...
323
+ ## Retrospective
324
+ ...
325
+ ```
326
+
327
+ `starts` and `ends` are reserved and required. Everything else — goal, capacity,
328
+ committed/completed points, PI objectives, retro notes — is configurable
329
+ per period type.
330
+
331
+ ```bash
332
+ lpm new increment -t "2026 H2" --starts 2026-07-01 --ends 2026-12-31
333
+ lpm new sprint -t "Sprint 1" --starts 2026-08-03 --ends 2026-08-14 -p TL-1
334
+
335
+ lpm move LP-4 --period TL-2 # schedule
336
+ lpm move LP-4 --period none # unschedule
337
+ lpm move TL-3 --parent TL-4 # re-parent a period
338
+ ```
339
+
340
+ ### Which period is running
341
+
342
+ A period runs when today falls inside it, and that is all most boards ever need.
343
+ But plenty of teams do not plan by date, and every team occasionally needs to
344
+ reroute people mid-sprint — so there is a switch held over the calendar:
345
+
346
+ ```bash
347
+ lpm period TL-2 # how it stands right now
348
+ lpm period TL-2 --on # run it whatever the dates say
349
+ lpm period TL-2 --off # park it
350
+ lpm period TL-2 --dates # take the switch off; the calendar decides again
351
+ lpm period TL-2 --start-now # move it to start today, keeping how long it runs
352
+ ```
353
+
354
+ `--on` and `--off` write one optional reserved field, `active`, on the period
355
+ document; `--dates` removes it. **Absent is the normal state** — a board that
356
+ never touches the switch behaves exactly as it always did.
357
+
358
+ The two directions are deliberately not symmetric. Switching a period **off**
359
+ parks everything nested inside it: "not this quarter" would mean nothing if its
360
+ sprints kept running. Switching one **on** speaks for that timebox alone,
361
+ because a live quarter has never meant all six of its sprints are this week.
362
+
363
+ What it changes is *what gets offered*, never what is reachable. Work in a
364
+ switched-off period sinks below even unscheduled work in `lpm task next`, the
365
+ MCP `next_tasks` and the queue — which is what makes the switch a way to steer
366
+ a team at short notice. Nothing is hidden, and no document becomes unreadable.
367
+
368
+ `--start-now` is the other half: it rewrites dates rather than overriding them.
369
+ The period takes today and keeps how long it runs, every period nested inside it
370
+ moves by the same number of days so a restarted increment keeps its shape,
371
+ whatever else was running is closed yesterday, and the periods above stretch to
372
+ reach. In the web UI both live on every box in the Periods tab: a toggle, and a
373
+ "Start now" button that says what it is about to change before it changes it.
374
+
375
+ ### When a sprint overruns
376
+
377
+ A period whose end date has passed while work in it is still open is flagged in
378
+ red, on the CLI and in the Periods tab. There are exactly two honest answers,
379
+ and both are offered rather than one being chosen for you:
380
+
381
+ ```bash
382
+ lpm period TL-2 --complete # move the open issues to the board's end state
383
+ lpm period TL-2 --carry-over # move them into the next period beside it
384
+ ```
385
+
386
+ Completing records that the team stopped, not that the work happened. Carrying
387
+ over leaves what was finished where it was delivered — that is the record of the
388
+ sprint — and moves the rest one period along. Neither invents a period, so
389
+ carrying work down a run is what makes the last sprint's backlog grow, which is
390
+ the fact worth seeing. When there is no period after the one being corrected,
391
+ the command refuses rather than quietly unscheduling the work.
392
+
393
+ Both act only on issues scheduled *directly* in the period: an increment answers
394
+ for its own epics, and the sprints inside it answer for their own stories.
395
+
396
+ [`docs/periods.md`](docs/periods.md) is the full reference: how the dates and the
397
+ switch combine, exactly what each bucket of the work queue holds, and what
398
+ restarting or correcting a period moves.
399
+
400
+ Because the hierarchies are independent, a feature can sit in an increment while
401
+ its stories sit in individual sprints — the transversal cut. `lpm check`
402
+ validates that periods have real dates, that `ends` is not before `starts`, that
403
+ a child period fits inside its parent, that siblings don't overlap, and that
404
+ every issue's `period` points at a period that exists.
405
+
406
+ It also warns when **an issue is scheduled before something it depends on**:
407
+
408
+ ```
409
+ warn scheduled in TL-2 (2026-08-03) but depends on LP-4,
410
+ scheduled later in TL-3 (2026-08-17)
411
+ ```
412
+
413
+ That check is the reason both features earn their keep together.
414
+
415
+ Periods are opt-in. Drop `period_prefix`, `period_hierarchy` and `period_types`
416
+ from the config and the timeline disappears; the `blank` template ships without
417
+ them.
418
+
419
+ ## Team and resources
420
+
421
+ Resources answer "who does the work?". They live under `.lpm/team`, are
422
+ configured exactly like issue and period types, and come in two flavours:
423
+
424
+ | | What it is | Example |
425
+ | --- | --- | --- |
426
+ | **named** | A person | Alice Smith |
427
+ | **generic** | A pool of interchangeable people | "a jr. software developer", "a data scientist, any level" |
428
+
429
+ Which one a document is comes from its **type**, not from the document —
430
+ a resource type declared `generic: true` describes a pool:
431
+
432
+ ```yaml
433
+ resource_types:
434
+ person:
435
+ label: Person
436
+ role:
437
+ label: Role
438
+ generic: true
439
+ ```
440
+
441
+ ```markdown
442
+ ---
443
+ id: RS-4
444
+ type: role
445
+ title: Jr. software developer
446
+ capacity: 3
447
+ covers: []
448
+ created: 2026-07-31T16:12:02.104Z
449
+ author: Jane Doe <jane@example.com>
450
+ discipline: backend
451
+ level: junior
452
+ skills: [typescript, sql]
453
+ ---
454
+
455
+ ## What this pool covers
456
+ ```
457
+
458
+ `capacity` is full-time equivalents: `1` a full-timer, `0.5` someone part-time,
459
+ `3` a pool of three. `0` means unavailable, and `lpm check` warns if you assign
460
+ work to them anyway.
461
+
462
+ ```bash
463
+ lpm new person -t "Alice Smith" --set email=alice@example.com
464
+ lpm new role -t "Jr. software developer" --capacity 3 --set level=junior
465
+ lpm new person -t "Bob Jones" --capacity 0.5
466
+
467
+ lpm move LP-4 --assignee RS-1 # to a person
468
+ lpm move LP-4 --assignee "Jr. soft" # by name, or a unique prefix of one
469
+ lpm move LP-4 --assignee none # back to nobody
470
+ ```
471
+
472
+ Park work in a pool and anyone who **covers** that pool can pick it up:
473
+
474
+ ```bash
475
+ lpm link RS-1 --covers RS-4 # Alice can work as a junior developer
476
+ lpm link RS-1 --covers RS-4 --remove
477
+ ```
478
+
479
+ Coverage stores only the forward edge, exactly like `depends_on`; the inverse is
480
+ derived at load time into `board.coveredBy`. It is one hop and not transitive: a
481
+ pool covering a pool does not chain.
482
+
483
+ `lpm team` shows who is carrying what:
484
+
485
+ ```
486
+ Roster
487
+ RS-1 Alice Smith person 1 FTE open 4 wip 1 done 2 story_points 13 covers RS-4
488
+ RS-2 Bob Jones person 0.5 FTE open 1 wip 0 done 0 story_points 3
489
+
490
+ Pools
491
+ RS-4 Jr. software developer pool 3 FTE open 7 wip 0 done 1 story_points 21 covered by RS-1
492
+ RS-5 Sr. data engineer pool 1 FTE open 3 wip 0 done 0 story_points 8 nobody covers this
493
+
494
+ -- (unassigned) open 2 wip 0 done 0 story_points 5
495
+
496
+ Total 5.5 FTE · 17 open · 1 in progress · 50 story_points · 9.1 per FTE
497
+
498
+ warn RS-5 (Sr. data engineer) holds open work but nobody covers it
499
+ warn 2 open issue(s) have no assignee
500
+ ```
501
+
502
+ `lpm team --period TL-2` scopes it to one sprint (and its child periods), which
503
+ is where over-commitment actually shows up. Only **work units** are counted, so a
504
+ feature does not double-count the stories under it, and a story marked
505
+ [`atomic`](#the-unit-of-work) does not double-count its own sub-tasks.
506
+
507
+ This is deliberately a **load view, not a scheduler**: it reports demand against
508
+ declared capacity, and names the two situations that mean work simply cannot
509
+ happen — a pool nobody covers, and open work nobody owns. What "too much" means
510
+ for your team stays your call.
511
+
512
+ The roster is opt-in the same way the timeline is: drop `resource_prefix`,
513
+ `resource_hierarchy` and `resource_types` and `team/` disappears.
514
+
515
+ ### Squads
516
+
517
+ A **squad** is a named sub-team of resources — "Frontend", "Platform", "Data".
518
+ When a period is owned by a squad, only that squad's members are offered work
519
+ from it. This lets two teams run independent plans inside one board: one
520
+ increment per squad, sprints grouped under it, and `lpm task next` shows each
521
+ person their squad's work.
522
+
523
+ Squads are configured exactly like the rest, and opt-in the same way:
524
+
525
+ ```yaml
526
+ squad_prefix: SP
527
+ squad_types:
528
+ squad:
529
+ label: Squad
530
+ attributes: {}
531
+ squad_hierarchy:
532
+ - squad
533
+ ```
534
+
535
+ A squad document lists its members:
536
+
537
+ ```markdown
538
+ ---
539
+ id: SP-1
540
+ type: squad
541
+ title: Frontend
542
+ members: [RS-1, RS-5, RS-8]
543
+ created: 2026-08-03T12:00:00.000Z
544
+ author: Alice Smith <alice@example.com>
545
+ ---
546
+ ```
547
+
548
+ Assign a squad to a period (`lpm set TL-1 --squad SP-1`), and the period's work
549
+ is gated through the routing. A sprint with no squad inherits from its
550
+ increment, the same way the `active` switch cascades; a sprint can override with
551
+ its own.
552
+
553
+ The squad engine lives in `src/core/board/query.ts` (`effectiveSquad`),
554
+ `board/tasks/ranking.ts` (the `candidatesFor` gate), and `board/load.ts`
555
+ (`periodSquadMembers`). The web UI manages squads in the Team drawer.
556
+
557
+ ## Working as a team member
558
+
559
+ Tell light-plan who you are, then ask it what to do:
560
+
561
+ ```bash
562
+ lpm me "Alice Smith" # or `lpm me RS-1`; `lpm me` alone prints it
563
+ lpm task next # what to work on, best first
564
+ lpm task start # claim the top one: assign it to you, start it
565
+ lpm task current # what you have in flight
566
+ lpm task done # move it to the board's end state
567
+ lpm task prev # what you finished most recently
568
+ ```
569
+
570
+ ```
571
+ $ lpm task next
572
+ Next up for RS-1 Alice Smith
573
+ LP-12 Guest checkout User Story · ready TL-2
574
+ LP-9 Cart totals User Story · backlog TL-2 · pool RS-4
575
+
576
+ Claim the first with lpm task start
577
+ ```
578
+
579
+ `lpm task next` offers **work units** that are assigned to you, or parked in a
580
+ pool you cover, and that nothing unfinished is blocking. Add `--unassigned` to
581
+ include work nobody owns. Containers (an epic with children) are never offered —
582
+ the work units under them carry the work. Which issues are units is a config
583
+ question; see [the unit of work](#the-unit-of-work).
584
+
585
+ Work in a period somebody has [switched off](#which-period-is-running) is **not
586
+ offered at all** — off means "not this one", and ranking it last would only
587
+ delay it until the rest of the queue emptied. `--parked` considers it anyway,
588
+ and `lpm open`/`lpm task start <id>` reach it by id regardless: this is routing,
589
+ not permission.
590
+
591
+ The order of what remains is: the running or overdue period first, then
592
+ unscheduled work, then periods that have not started; within that, the board's
593
+ `priority_attribute`, then the column closest to done, then **the part of the
594
+ plan that is already under way**, then how much each issue unblocks, then age.
595
+
596
+ That middle step is what keeps a queue from handing out one story from every
597
+ feature in turn. When nothing a person set by hand separates two stories, the
598
+ one whose feature somebody is already inside comes first, then the one whose
599
+ feature is closest to finished, and work in an untouched feature comes last.
600
+ The question is asked of the outermost level the two do not share, so an epic
601
+ under way is preferred before its features are ever compared, and two stories
602
+ in the same feature are never separated by it.
603
+
604
+ `lpm task start [id]` assigns the issue to you and moves it to the first status
605
+ marked `active: true`; `lpm task done [id]` moves it to the first `terminal: true`
606
+ one. Both take an id, and both default to the obvious one — the top
607
+ recommendation for `start`, your single in-flight issue for `done`. Claiming
608
+ someone else's issue needs `--force`.
609
+
610
+ #### Claiming is atomic
611
+
612
+ `lpm task next` is a recommendation, and between reading it and acting on it
613
+ somebody else may have taken the same issue — another developer in the same
614
+ checkout, an agent, a `lpm queue agent` run. So `start` is not "write my name on
615
+ it". It takes the board's write lock, **re-reads the board**, checks the issue is
616
+ still free as it stands on disk, and only then writes:
617
+
618
+ ```
619
+ $ lpm task start LP-12
620
+ error LP-12 is already being worked on by Bob Chen (RS-2)
621
+ It is "in_progress" on the board as it stands now, and Alice Smith (RS-1) is asking for it.
622
+ Force takes it off somebody who is part-way through it. Take something else instead.
623
+ ```
624
+
625
+ The claim is recorded in the issue document itself, not only in its frontmatter:
626
+
627
+ ```yaml
628
+ ---
629
+ id: LP-12
630
+ assignee: RS-1
631
+ status: in_progress
632
+ ---
633
+ ```
634
+
635
+ ```markdown
636
+ <!-- lpm:activity -->
637
+
638
+ ## Activity
639
+
640
+ ### 2026-08-16T09:14:02.104Z — Alice Smith (RS-1) — claimed
641
+ ```
642
+
643
+ The frontmatter is what withholds the issue from everybody else's queue — work
644
+ in an active status is never offered, and the rest is routed by assignee — and
645
+ the activity line is how a person reading `_issue.md` in a file tree, or a diff
646
+ in the board's git history, finds out who took it and when. The same is true of
647
+ the MCP `start_task` and of every task `lpm queue agent` picks up.
648
+
649
+ A run that loses a claim is not a run that failed. `lpm queue agent` asks the
650
+ queue again and takes the next thing; the issue it lost is left completely
651
+ untouched — no flag, no comment, no status change. If it loses several in a row
652
+ it stops and says so, rather than spinning against whoever is out-claiming it.
653
+
654
+ This is one half of what makes a `.lpm` folder safe to share; the other half is
655
+ [below](#several-people-and-agents-one-checkout).
656
+
657
+ #### A parent's status is derived
658
+
659
+ Nobody works a feature: the stories under it are what get worked, and the
660
+ feature is a name for them. So a container's status comes from its contents.
661
+ Finish the last open issue in a feature and the feature is finished too, and its
662
+ epic with it if that was its last open feature — as far up as it goes. Reopen
663
+ one and they reopen with it, and adding a new issue inside a finished container
664
+ reopens it as well.
665
+
666
+ `terminal: true` is the whole of what "finished" means here — nothing in
667
+ light-plan knows the word "done", it reads the flag. When a board declares more
668
+ than one end state, a parent closes into the one its children agree on, and into
669
+ the first one declared when they do not.
670
+
671
+ It happens wherever work is moved — `lpm task done`, `lpm move --status`, the
672
+ MCP `finish_task` and `update_document`, and pushing from the web app — because
673
+ it lives in the operation the three of them share. Each container it carries is
674
+ printed by the CLI, listed in `rolledUp` by the MCP tools, and recorded in the
675
+ parent's own activity section, so a status nobody typed always says where it
676
+ came from.
677
+
678
+ Two things follow. Anything waiting on a feature is offered the moment its last
679
+ story is closed, without anybody remembering to tick the feature off. And a
680
+ board where that never happened — frontmatter edited by hand, a merge that took
681
+ one side of a status, a board written before this existed — is repaired by
682
+ `lpm check --fix`, which reports every container out of step with its contents
683
+ and rolls it up.
684
+
685
+ Who you are is stored in `.lpm/local.json`, which `lpm init` adds to
686
+ `.lpm/.gitignore`: the board is shared, but who is at this keyboard is not.
687
+ `LPM_USER=RS-2 lpm task next` overrides it for one command.
688
+
689
+ ### Running the queue forward: `lpm queue simulate`
690
+
691
+ `lpm task next` answers "what now?" one step at a time. `lpm queue simulate`
692
+ answers the other question — *if this one person were the only contributor,
693
+ what would they work on, and in what order?* It takes the top of the queue,
694
+ marks it finished in memory, asks again, and keeps going until nothing is left
695
+ that they could pick up:
696
+
697
+ ```bash
698
+ lpm queue simulate # you
699
+ lpm queue simulate --user "Alice Smith" # a person on the roster
700
+ lpm queue simulate --role "QA engineer" # a pool: anyone working out of it
701
+ lpm queue simulate --user alice --skipped --unassigned --limit 20
702
+ ```
703
+
704
+ ```
705
+ $ lpm queue simulate --user alice
706
+ Queue simulation for RS-1 Alice Smith (person)
707
+
708
+ id title type effort total
709
+ 1. LP-12 Guest checkout User Story 3 3 TL-2 · frees LP-14
710
+ 2. LP-9 Cart totals User Story 2 5 TL-2 · pool RS-4
711
+ 3. LP-14 Apply a promo code User Story 5 10 TL-2
712
+
713
+ Total 3 tasks · 10 story_points
714
+ 2 open issues never reached — see them with lpm queue simulate --skipped
715
+ ```
716
+
717
+ `--user` names a person and `--role` names a pool (a resource type declared
718
+ `generic: true`). They are separate flags on purpose: simulating a pool as
719
+ though it were a person answers a question nobody asked, so the command says
720
+ which one you gave it rather than guessing.
721
+
722
+ **It is `lpm task next` in a loop, not a second opinion about it.** Every rule
723
+ about what may be picked up — routing, pools, work units, blockers, your
724
+ profile's scope, switched-off periods — lives in the engine and reaches the run
725
+ only through `nextTasks`, against a board with the earlier steps marked
726
+ finished. A developer stepping through `lpm task next` by hand gets this
727
+ sequence, and runs out where this runs out. That is the point of the command, so
728
+ it is also what the tests pin down.
729
+
730
+ Two consequences worth knowing:
731
+
732
+ - **Work already in progress goes first — unless it is flagged.** The queue does
733
+ not offer what has already been picked up, so a run that ignored it would
734
+ report everything waiting on it as blocked forever. A flag is where that stops:
735
+ it says the work *has* stopped and needs a person, and nobody else is in this
736
+ run to clear it, so starting from it would assume away the very thing holding
737
+ the queue up. Flagged work is listed under `--skipped` instead, and counted
738
+ under the run, which is what makes this agree with `lpm task next` and
739
+ `lpm queue agent` on a stalled board. `lpm queue agent` resumes in-flight work
740
+ by the same rule, so the prediction is of the run and not of a different
741
+ reading of the board.
742
+ - **Nothing is written and no clock moves.** The board is untouched, and `today`
743
+ stays fixed for the whole run, so a period that has not started stays
744
+ unstarted. This reports an *order*, never a schedule: light-plan does not know
745
+ how long a task takes, so it adds up effort and stops there.
746
+ - **Parked sprints are left out and counted.** Whatever the switch withholds
747
+ from `lpm task next` is withheld here too, and the total says how much, so a
748
+ short run is never a mystery. `--parked` includes it.
749
+
750
+ `--skipped` is the other half of the answer. Nobody else is contributing, so
751
+ work held by someone else is never finished and anything waiting on it waits
752
+ forever — which is usually the thing you opened the command to find out:
753
+
754
+ ```
755
+ $ lpm queue simulate --user alice --skipped
756
+ ...
757
+ Never reached
758
+ LP-20 Settlement report assigned to RS-2 (Bob Jones)
759
+ LP-21 Reconcile the ledger waiting on LP-20
760
+ LP-22 Refund flow already in progress under RS-4 (Web developer)
761
+ LP-23 Import legacy carts assigned to nobody — try --unassigned
762
+ LP-24 Audit the payment providers flagged by RS-1 (Alice Smith) — the work has stopped until somebody clears it
763
+ ```
764
+
765
+ If you use a [profile](#profiles-giving-one-developer-one-part-of-the-board),
766
+ its scope narrows a simulation of **you**, because that is the queue you are
767
+ actually offered. Simulating somebody else uses the whole board: you do not hold
768
+ their profile.
769
+
770
+ Like `lpm team`, this reports — it never levels load, assigns anything or writes
771
+ a date.
772
+
773
+ ### Draining the queue with an agent: `lpm queue agent`
774
+
775
+ `lpm queue simulate` predicts the order; `lpm queue agent` *works* it. It takes
776
+ the top of the queue, hands the issue's brief (the same one `lpm instructions`
777
+ prints) to an isolated [pi](https://pi.dev) coding-agent run with a fresh
778
+ context, reads back what the agent reports, and records the outcome the way a
779
+ person would — `lpm task done` on success, a flag on failure — then picks the
780
+ next task and repeats.
781
+
782
+ ```bash
783
+ lpm queue agent --user alice --max-tasks 3 # do the next three, as Alice
784
+ lpm queue agent --model anthropic:claude-opus-4-5 --effort high
785
+ lpm queue agent --commit task # commit after each finished task
786
+ lpm queue agent --file agent.yml # take options from a YAML file
787
+ lpm queue agent --dry-run # show the next pick and its brief
788
+ ```
789
+
790
+ | Option | Meaning |
791
+ | --- | --- |
792
+ | `--user <id\|name>` | Route the queue to this person (default: `lpm me`) |
793
+ | `--max-tasks <n>` | Stop after this many tasks are picked up |
794
+ | `--model <spec>` | Pi model, as `provider:model` |
795
+ | `--effort <level>` | Thinking level: `off … max` |
796
+ | `--commit <mode>` | `none` (default), `task`, or `parent` |
797
+ | `--unassigned` / `--parked` | Widen the queue, exactly as `lpm task next` does |
798
+ | `--timeout <secs>` | Per-task wall-clock cap |
799
+ | `--command-timeout <secs>` | Kill any single shell command after this long (default 300) |
800
+ | `--plain` | Log one line per event instead of drawing the live view |
801
+ | `--file <path>` | Read all of the above from YAML (CLI flags win) |
802
+ | `--dry-run` | Pick and brief the next task, but run and write nothing |
803
+
804
+ It works the **same queue** `lpm task next` offers — routing, scope, work units,
805
+ blockers and parked periods all still apply — and it finishes every piece of work
806
+ under one parent before moving to the next, so a feature lands together. Each run
807
+ leaves a comment on the issue and a full JSON log under `.lpm/runs/` (tools used
808
+ and in what order, timing, tokens and cost when the provider reports them). On
809
+ failure the issue is flagged for a human and the run moves on; a flagged task is
810
+ never retried.
811
+
812
+ Before it asks for anything new, a run **carries on with what this person is
813
+ already holding**: a task an earlier run claimed and did not finish — a crash, a
814
+ timeout, a run you interrupted — is left in progress, and the queue never offers
815
+ work in progress, so nothing else would ever return to it and everything waiting
816
+ on it would stay blocked. This is the same rule `lpm queue simulate` seeds its
817
+ prediction with, so the two commands cannot disagree about it.
818
+
819
+ A flag is where that stops, and because of it a run can stop with the board
820
+ apparently full of work: everything left is waiting on issues this person is
821
+ already holding, and those are flagged. The run says so rather than just
822
+ reporting an empty queue — it lists what they hold, marks which of it is flagged,
823
+ and points at the comments that explain why. Clearing the flag
824
+ (`lpm flag clear <id> -m "..."`) hands the work back to the next run; finishing
825
+ it yourself does the same.
826
+
827
+ #### Watching a run
828
+
829
+ On a terminal the run draws a small pane at the bottom of the screen, on stderr,
830
+ and rewrites it in place:
831
+
832
+ ```
833
+ LP-14 Guest checkout form validation task 2 · 4m 31s
834
+ agent working · bash npm test -- --run src/checkout 7 tools
835
+ ───────────────────────────────────────────────────────────────────────────
836
+ ── LP-14 Guest checkout form validation
837
+ Adding the validator and a test for it.
838
+ ❯ bash npm test -- --run src/checkout
839
+ ✓ src/checkout/validate.test.ts (4 tests)
840
+ Test Files 1 passed
841
+ ↑↓ PgUp/PgDn scroll · Ctrl+C stop 12 lines back
842
+ ```
843
+
844
+ The top two lines say which task, which stage of it (claiming, briefing, agent
845
+ working, recording, committing), and what the agent is running right now. Under
846
+ them is the tail of the run: what the agent is saying, the tools it calls and
847
+ what they print. **↑/↓** and **PgUp/PgDn** scroll back through it — the view
848
+ holds its place while new output arrives, and starts following again when you
849
+ reach the end. **Ctrl+C** stops the run; the task stays in progress and the next
850
+ run picks it back up.
851
+
852
+ The pane is a window, not a transcript. The whole story of a task is on the
853
+ issue: a comment when it ends, and the full JSON log under `.lpm/runs/`.
854
+
855
+ Redirect the output — or pass `--plain` — and there is no pane at all: one line
856
+ per event, no escape codes, which is what you want in CI or a log file. The
857
+ report at the end always goes to stdout, so `lpm queue agent > run.txt` is a
858
+ clean file either way.
859
+
860
+ #### A run nobody is watching
861
+
862
+ The point of the command is that it does not stop, so the agent's shell is set
863
+ up for a terminal with nobody at it:
864
+
865
+ - **Pagers print and exit** and **editors return immediately** (`PAGER`,
866
+ `GIT_PAGER`, `GIT_EDITOR`, `EDITOR`, `VISUAL`, …), so `git log`, a `git commit`
867
+ with no `-m`, and `lpm open LP-4` — which spawns `$VISUAL`/`$EDITOR` and waits
868
+ for the window to close — cannot sit there waiting for a keypress.
869
+ - **`CI=true`**, which is how a test runner is told to run once instead of
870
+ starting in watch mode, and how most scaffolders are told not to ask
871
+ questions.
872
+ - **A command that would open a file in another program is refused** — `start`,
873
+ `open`, `xdg-open`, `code`, `explorer`, `less`, `man`, `vim`, `tail -f`,
874
+ `Start-Process`, `git add -p` and their like. The agent gets an error saying
875
+ what was refused and what to do instead, and carries on.
876
+ - **Everything else is killed at `--command-timeout`** (300s by default). A
877
+ command the agent gives its own timeout keeps that one; this only fills in a
878
+ cap where there was none.
879
+
880
+ This is a guard against a stuck run, not a sandbox: the agent still has a real
881
+ shell in your project, and an agent that wants to get around the refusal can. It
882
+ is there because one `start report.md` used to hold a whole queue open until
883
+ somebody noticed.
884
+
885
+ `--commit` decides what happens to the code the agent wrote in your project:
886
+ `none` leaves it in the working tree for you to review, `task` commits after each
887
+ finished task, and `parent` commits once *all* the work under a task's parent is
888
+ done. Commits are made in the **project** repo, not the `.lpm` board — commit the
889
+ board (statuses, comments, run logs) yourself.
890
+
891
+ `lpm queue agent` is **experimental**, so the pi agent it drives is not installed
892
+ with light-plan (it needs Node 22.19+). Install it beside light-plan — with `-g`
893
+ when light-plan is installed globally, without it in a project:
894
+
895
+ ```bash
896
+ npm install -g @earendil-works/pi-coding-agent @earendil-works/pi-ai
897
+ # or, through npx:
898
+ npx -p light-plan -p @earendil-works/pi-coding-agent -p @earendil-works/pi-ai lpm queue agent
899
+ ```
900
+
901
+ `--model` takes `provider:model` for **any provider pi supports** — Anthropic,
902
+ OpenAI, DeepSeek, Google, Groq, Mistral, xAI, and others. Set that provider's API
903
+ key in the environment and pi picks it up; omit `--model` to use pi's default.
904
+
905
+ ```bash
906
+ export DEEPSEEK_API_KEY=... # or ANTHROPIC_API_KEY, OPENAI_API_KEY, ...
907
+ lpm queue agent --model deepseek:deepseek-v4-pro --max-tasks 1
908
+ ```
909
+
910
+ (Exact model ids come from the installed pi version's catalog; the pi CLI or its
911
+ docs list what each provider offers.)
912
+
913
+ ⚠️ The agent runs with real `bash` and `write` tools on your project. Running it
914
+ unattended is a decision you make; review what it produces, and prefer
915
+ `--max-tasks` and `--dry-run` while you learn how it behaves on your board.
916
+
917
+ ## When the work stops: flags
918
+
919
+ Sometimes work you have picked up cannot go on. A credential expired, a decision
920
+ has not been made, a question needs somebody who knows the area. Moving the issue
921
+ back to the backlog would be a lie — you are holding it — and leaving it in
922
+ progress is a quieter one, because the board goes on saying it is being worked.
923
+
924
+ A **flag** says the third thing:
925
+
926
+ ```bash
927
+ lpm flag --comment "Sandbox credentials expired; asked ops on #infra"
928
+ lpm flag LP-12 --reason help --comment "Need a decision on the retry budget"
929
+ lpm flag list # everything stopped, across the board
930
+
931
+ # the plan owner's side:
932
+ lpm flag clear LP-12 --comment "New credentials in the vault; carry on"
933
+ ```
934
+
935
+ | Reason | Means |
936
+ | --- | --- |
937
+ | `blocked` (default) | something outside this issue has to happen first |
938
+ | `paused` | deliberately set down; the work is fine, the timing is not |
939
+ | `help` | a person is needed — a decision, a review, a pair of eyes |
940
+
941
+ A flag is **not a status**. The issue keeps its column and its assignee: it is
942
+ still yours, still in progress, and the flag says it is not moving. That is a
943
+ different claim from "nobody has started this", and it is the only one that needs
944
+ somebody's attention today.
945
+
946
+ What it does change is what you are *offered*. `lpm task next`, `lpm task start`
947
+ and the agent queue all leave flagged work out, whatever column it sits in — so
948
+ "blocked, do not start this" on a backlog issue is heard rather than displayed
949
+ and ignored. Like a switched-off period, it steers the queue and hides nothing:
950
+ `lpm open`, `lpm task start <id>` and `lpm flag list` all still reach it.
951
+
952
+ **A flag carries up the plan.** Nobody scrolls to the bottom of an epic to find
953
+ out whether anything under it has stopped, so every container above a flagged
954
+ issue is marked `inside` ("Stopped inside") — and the mark comes off by itself
955
+ when the last stopped thing inside it starts moving again, whether you cleared
956
+ the flag or finished the work. It is the same roll-up a closed story does to its
957
+ feature, and it is written and cleared automatically:
958
+
959
+ ```bash
960
+ lpm flag LP-42 -m "Sandbox credentials expired" # LP-42 blocked; the feature,
961
+ # epic and program say "Stopped inside"
962
+ lpm flag clear LP-42 -m "New credentials issued" # and all three go quiet again
963
+ ```
964
+
965
+ `inside` is not a reason you can raise — `lpm flag --reason inside` is refused —
966
+ because it is what tells a container the roll-up may clear from one you paused
967
+ by hand. A flag you put on a feature yourself is never overwritten and never
968
+ cleared for you. `lpm flag list` shows the issues somebody actually stopped and
969
+ counts the containers standing in front of them separately, so the list stays
970
+ the short one you can act on.
971
+
972
+ **The comment is required, both ways.** A red box nobody can read is a round trip
973
+ to ask what it means, which is the trip the flag exists to save — so `lpm flag`
974
+ refuses without one and writes it into the issue's `_comments.md` in the same
975
+ call. Clearing needs one too: whoever raised the flag is the person who reads it.
976
+
977
+ **And every flag change is written into the document itself, comment or not.**
978
+ The flag lands in the frontmatter, and the raise or the clear lands as a dated,
979
+ attributed line in the issue's activity section:
980
+
981
+ ```markdown
982
+ <!-- lpm:activity -->
983
+
984
+ ## Activity
985
+
986
+ ### 2026-08-16T09:14:02.104Z — Alice Smith (RS-1) — flagged: Blocked
987
+
988
+ **Flagged: Blocked**
989
+
990
+ Sandbox credentials expired
991
+
992
+ ### 2026-08-16T14:41:55.881Z — Alice Smith (RS-1) — flag cleared
993
+
994
+ **Flag cleared** (was: Blocked)
995
+
996
+ New credentials issued
997
+ ```
998
+
999
+ That matters most for the flag changes **nobody typed a comment for**, which on
1000
+ a working board are the majority: the container that gained `inside` because a
1001
+ story four levels down stopped, the same container going quiet again, the flag
1002
+ that finishing the work answered, the one `lpm check --fix` wrote to repair a
1003
+ container that had drifted. None of those writes a comment — `_comments.md` is
1004
+ where a *person* explains a stall, and one derived entry per ancestor per flag
1005
+ would bury the explanation the flag exists to carry — so the activity line is
1006
+ the entire record. Without it a node turns red and back again with nothing in
1007
+ `_issue.md`, and nothing in the board's git history, saying when or why:
1008
+
1009
+ ```
1010
+ $ git -C .lpm log -p -- board/LP-1/LP-2/LP-3/_issue.md
1011
+ +flag: inside
1012
+ +### 2026-08-16T09:14:02.104Z — Alice Smith (RS-1) — flagged: Stopped inside
1013
+ +
1014
+ +Work inside this has stopped — see LP-42.
1015
+ ```
1016
+
1017
+ Flagged issues are drawn **in red** on the canvas, a container standing in front
1018
+ of one is drawn in a quieter red, and a collapsed node says how many are stuck
1019
+ inside it — folding a feature must not hide a story that has stopped. Finishing an issue clears any flag on it automatically; the comment
1020
+ trail keeps the history.
1021
+
1022
+ Clearing is the plan owner's call, by convention rather than by enforcement.
1023
+ light-plan has no permissions anywhere — [a profile routes work, it is not access
1024
+ control](#profiles-giving-one-developer-one-part-of-the-board) — and inventing
1025
+ them here would make that untrue. What the tooling does is record who did it.
1026
+
1027
+ ## Working briefs: the context to actually do it
1028
+
1029
+ `lpm task next` says *what* to work on. `lpm instructions` answers the question
1030
+ straight after it — *what do I need to know to start?*
1031
+
1032
+ ```bash
1033
+ lpm instructions LP-12 # markdown on stdout, and nothing else
1034
+ lpm instructions # the issue you have in progress
1035
+ lpm instructions LP-12 > brief.md
1036
+ ```
1037
+
1038
+ A story on its own rarely explains itself. So the brief walks up the hierarchy
1039
+ and lays out the **title and body of every ancestor** — the programme, the epic,
1040
+ the feature — above the issue's own, then adds its breakdown, what it is blocked
1041
+ by, the research it rests on, and its work log:
1042
+
1043
+ ```
1044
+ $ lpm instructions LP-12
1045
+ # LP-12 — Pay as a guest
1046
+
1047
+ You are picking up a user story on the acme board. …
1048
+
1049
+ ## Epic: Checkout (LP-4)
1050
+
1051
+ ### Summary
1052
+
1053
+ Buy things without an account. …
1054
+
1055
+ ## Feature: Guest flow (LP-9)
1056
+ …
1057
+ ## The story: Pay as a guest
1058
+
1059
+ ### Acceptance Criteria
1060
+
1061
+ - [ ] Given a signed-out shopper …
1062
+ ```
1063
+
1064
+ Everything about *how* the brief was made goes to stderr, so the brief itself
1065
+ pipes cleanly into a prompt, a file or a clipboard.
1066
+
1067
+ Agents get the same text from the MCP tool `get_instructions { id }`, which is
1068
+ what the shipped `lpm-developer` agent reads before writing any code.
1069
+
1070
+ ### Context templates
1071
+
1072
+ The layout is a board-level decision, like the statuses. Layouts live in
1073
+ `.lpm/templates/context/<issue type>.md`, with `default.md` behind them and a
1074
+ built-in layout behind that — so a board with no templates at all still gets the
1075
+ ancestors' titles and bodies above the issue's own. `lpm init` writes starters
1076
+ for the types your config declares:
1077
+
1078
+ ```bash
1079
+ lpm instructions --list # which layout each issue type resolves to
1080
+ lpm instructions --init # write the starters (never overwrites)
1081
+ lpm instructions --audit # read every layout for risky code
1082
+ lpm instructions --template ./one-off.md LP-12
1083
+ ```
1084
+
1085
+ They are [Eta](https://eta.js.org) templates — EJS syntax, ordinary JavaScript
1086
+ between the tags:
1087
+
1088
+ ```markdown
1089
+ # <%= issue.id %> — <%= issue.title %>
1090
+
1091
+ <% if (epic) { %>
1092
+ ## Epic: <%= epic.title %>
1093
+
1094
+ <%= heading(epic.body, 3) %>
1095
+
1096
+ <% } %>
1097
+ ## The story
1098
+
1099
+ <%= heading(issue.body, 3) %>
1100
+
1101
+ <% for (const task of children) { %>
1102
+ - <%= task.id %> <%= task.title %> — <%= task.status_label %>
1103
+ <% } %>
1104
+ ```
1105
+
1106
+ **Every issue type your board declares is a variable**, resolving to the nearest
1107
+ ancestor of that type — so one template can say "this story's epic" without
1108
+ knowing how deep it sits. `heading(n)` re-levels a body so it nests under the
1109
+ heading above it, leaving fenced code alone. Note that an empty array is truthy
1110
+ in JavaScript: guard a list with `.length`, an optional document with the name
1111
+ alone. The full reference, including every value and helper, is in
1112
+ [docs/context-templates.md](docs/context-templates.md) and in
1113
+ `lpm instructions --help`.
1114
+
1115
+ Templates are not board truth: they live under `.lpm/templates`, `lpm check` does
1116
+ not know they exist, and no template can make a board invalid.
1117
+
1118
+ ### ⚠ Context templates are code
1119
+
1120
+ Eta compiles a template into a JavaScript function and **runs it**. A `.lpm`
1121
+ folder arrives over `git pull` from whoever wrote it, so rendering somebody
1122
+ else's board can run somebody else's JavaScript, as you, with your filesystem and
1123
+ your environment variables.
1124
+
1125
+ light-plan reads every template before compiling it and refuses the known
1126
+ escapes — `process`, `require`, `this`, `import()`, a property reached by a
1127
+ computed key, and Eta's own file-reading `include`. The check tokenizes with
1128
+ Eta's own parser and parses the code with [acorn](https://github.com/acornjs/acorn),
1129
+ so `x["cons" + "tructor"]` is caught as readily as `require`:
1130
+
1131
+ ```
1132
+ $ lpm instructions LP-12
1133
+ error Refusing to render .lpm/templates/context/user_story.md: it can do more than lay out an issue
1134
+ 3:5 danger reaches outside the board (process) — process
1135
+ 4:5 danger `this` is the template engine itself, not the issue — this
1136
+ ```
1137
+
1138
+ `lpm instructions --audit` runs that check over the whole board and exits 1 on a
1139
+ finding, so it drops into CI beside `lpm check`. `--unsafe` renders anyway, for a
1140
+ template you wrote and meant; the MCP tool has no equivalent, because letting an
1141
+ agent opt out is the whole hole.
1142
+
1143
+ **This refuses the known escapes; it cannot make an untrusted template safe.**
1144
+ Read the templates that arrive with a board you did not write, the way you would
1145
+ read a `postinstall` script. Use at your own risk.
1146
+
1147
+ ## Profiles: giving one developer one part of the board
1148
+
1149
+ A **profile** is a small YAML file you hand a developer — or an agent. It says
1150
+ who they are and which part of the board they should be offered:
1151
+
1152
+ ```yaml
1153
+ # alice.yml
1154
+ user: Alice Smith
1155
+
1156
+ scope:
1157
+ under: [LP-2] # only work at or below these documents
1158
+ exclude: [LP-9] # never these, nor anything below them
1159
+ types: [user_story] # only these issue types
1160
+ periods: [TL-2] # only work scheduled here, or in a child period
1161
+ ```
1162
+
1163
+ Every key is optional, and every one of them narrows: `under` on its own scopes
1164
+ someone to an epic, `exclude` on its own keeps them out of one, and the two
1165
+ together read as "this programme, but not that feature". A period folds its
1166
+ child periods in, so naming an increment includes its sprints. Write a starter
1167
+ file and start using it in one command:
1168
+
1169
+ ```bash
1170
+ lpm profile --init ~/.lpm/alice.yml --user "Alice Smith"
1171
+ lpm profile ./profiles/alice.yml # or point at one you were given
1172
+ lpm profile # what is in force, resolved against this board
1173
+ lpm profile --clear
1174
+ ```
1175
+
1176
+ ```
1177
+ $ lpm profile
1178
+ Profile /home/alice/.lpm/alice.yml
1179
+ .lpm/local.json
1180
+ user Alice Smith
1181
+ scope under LP-2 · not LP-9
1182
+ offers 14 issues of 63
1183
+ ```
1184
+
1185
+ Only the *path* is remembered, in `.lpm/local.json` beside the current user and
1186
+ git-ignored with it — the profile belongs to the developer, not to the board.
1187
+ `LPM_PROFILE=./profiles/bob.yml lpm task next` points at a different one for one
1188
+ shell, and `lpm mcp --profile <file>` does the same for one agent session.
1189
+
1190
+ **Scope decides what the board offers you, never what is reachable.** It
1191
+ narrows `lpm task next` and `lpm task start` with no id, and the MCP tools
1192
+ `next_tasks` and `list_documents`. It does not touch `lpm open`, `lpm set`,
1193
+ `get_document`, or `lpm task current` — work you have already picked up stays
1194
+ yours even if the scope it came from moves, and a dependency you cannot read is
1195
+ worse than a recommendation you should ignore.
1196
+
1197
+ So this is **routing, not access control**: the board is a folder of markdown
1198
+ that whoever holds the profile can read, and a profile decides what is handed to
1199
+ them. Use it to keep a team of ten out of each other's epics, or to give five
1200
+ agents five slices of one plan. Do not use it to keep a secret.
1201
+
1202
+ Whatever is in force is printed with the work it filters, so a short list is
1203
+ never a mystery:
1204
+
1205
+ ```
1206
+ $ lpm task next
1207
+ Next up for RS-1 Alice Smith
1208
+ scope under LP-2 · not LP-9
1209
+ LP-12 Guest checkout User Story · ready TL-2
1210
+ ```
1211
+
1212
+ If the file names something this board does not have, that is reported and the
1213
+ rest still applies — except that a stale `under` offers nothing rather than
1214
+ quietly widening to everything. A profile that does not parse at all is
1215
+ reported too, and light-plan carries on unscoped. Unknown keys are an error:
1216
+ `excludes:` would otherwise silently hand someone the whole board.
1217
+
1218
+ Where identity is concerned the most specific answer wins: `LPM_USER`, then the
1219
+ profile's `user:`, then `lpm me`. `lpm me` says which one is talking.
1220
+
1221
+ [`docs/profiles.md`](docs/profiles.md) is the full reference: every key, how the
1222
+ file is found, what each surface does and does not filter, and how to set up a
1223
+ team or a swarm of agents.
1224
+
1225
+ ## Working by hand
1226
+
1227
+ The CLI is a convenience, not a gatekeeper. You can create a folder and an
1228
+ `_issue.md` (or `_period.md`, or `_resource.md`) yourself — even one containing
1229
+ nothing but a heading — and then run:
1230
+
1231
+ ```bash
1232
+ lpm check --fix
1233
+ ```
1234
+
1235
+ which adopts it: allocates an id from the right counter, infers the type from
1236
+ its depth (when that level has only one type), takes the title from the
1237
+ `# heading` or the folder name, sets the default status, defaults a resource's
1238
+ `capacity` to 1, fills in `created` (from the file's first commit, falling back
1239
+ to its mtime) and `author` (from `git config user.name/email`), adds the
1240
+ attributes its type declares, dedupes link and coverage lists, renames the
1241
+ folder to the document's id, rolls every container's status up from the work
1242
+ inside it (see [a parent's status is derived](#a-parents-status-is-derived)),
1243
+ and rewrites `INDEX.md`.
1244
+
1245
+ Anything `--fix` cannot decide for you is reported and left alone — an ambiguous
1246
+ type, a duplicate id, a missing sprint date, an assignee who is not on the
1247
+ roster, an attribute whose value has the wrong type. Without `--fix`, `check`
1248
+ never writes anything.
1249
+
1250
+ ## INDEX.md: the board as a table of contents
1251
+
1252
+ A folder is named after its id, so the file tree says what is nested in what but
1253
+ not what any of it *is*. `.lpm/INDEX.md` is where the titles are — every
1254
+ document in the board, with a link to its file, nested exactly the way the
1255
+ folders are:
1256
+
1257
+ ```markdown
1258
+ # Board index
1259
+
1260
+ ## Issues
1261
+
1262
+ - [LP-1](board/LP-1/_issue.md) — Payments platform
1263
+ - [LP-2](board/LP-1/LP-2/_issue.md) — Checkout revamp
1264
+ - [LP-3](board/LP-1/LP-2/LP-3/_issue.md) — Guest flow
1265
+
1266
+ ## Timeline
1267
+
1268
+ - [TL-1](timeline/TL-1/_period.md) — 2026 H2
1269
+ - [TL-2](timeline/TL-1/TL-2/_period.md) — Sprint 1
1270
+
1271
+ ## Team
1272
+
1273
+ - [RS-1](team/RS-1/_resource.md) — Alice Smith
1274
+ ```
1275
+
1276
+ It is written by `lpm init` and rewritten by every command that adds, removes,
1277
+ moves, reparents or retitles a document — from the CLI, from the web UI's Push,
1278
+ and from an agent over MCP alike. Nothing has to be run to keep it current.
1279
+
1280
+ It is generated, not authored: the board is the documents, and the index is a
1281
+ reading of them. Edit it and your edit is overwritten by the next change; delete
1282
+ it and `lpm check --fix` puts it back. `lpm check` reports it when it has fallen
1283
+ behind — which is what a badly resolved merge conflict inside `.lpm` looks like.
1284
+
1285
+ Because it is markdown with relative links, GitHub, GitLab and every editor's
1286
+ preview render it as a clickable outline of the whole plan.
1287
+
1288
+ ## Configuration
1289
+
1290
+ `.lpm/config.yml` defines the board. `lpm init` copies one of the built-in
1291
+ templates; edit it whenever the process changes and re-run `lpm check`.
1292
+
1293
+ ```yaml
1294
+ version: 1
1295
+ key_prefix: LP # issue ids: LP-1, LP-2, ...
1296
+
1297
+ statuses: # the kanban columns, in board order
1298
+ - id: backlog
1299
+ label: Backlog
1300
+ - id: in_progress
1301
+ label: In Progress
1302
+ active: true # work in progress: where `lpm task start` moves an issue
1303
+ - id: done
1304
+ label: Done
1305
+ terminal: true # an end state: where `lpm task done` moves it, and
1306
+ # what a parent takes when everything inside it is
1307
+ # there — see "a parent's status is derived"
1308
+
1309
+ default_status: backlog # optional; defaults to the first status
1310
+
1311
+ priority_attribute: priority # optional; an enum, most important value first
1312
+ effort_attribute: story_points # optional; an int or float
1313
+
1314
+ hierarchy: # index = folder depth
1315
+ - program
1316
+ - epic
1317
+ - feature
1318
+ - [user_story, bug] # types on one line share a level
1319
+ - sub_task
1320
+
1321
+ issue_types:
1322
+ user_story:
1323
+ label: User Story
1324
+ atomic: true # the smallest unit the queue hands out
1325
+ attributes:
1326
+ story_points:
1327
+ type: int
1328
+ description: Relative size, Fibonacci
1329
+ priority:
1330
+ type: enum
1331
+ values: [critical, high, medium, low]
1332
+ default: medium
1333
+ body: |
1334
+ As a **<role>**, I want **<capability>**, so that **<benefit>**.
1335
+
1336
+ ## Acceptance Criteria
1337
+
1338
+ - [ ] **Given** <context> **when** <action> **then** <outcome>
1339
+
1340
+ # --- time hierarchy (optional, same shape) ---
1341
+ period_prefix: TL # period ids: TL-1, TL-2, ... must differ from key_prefix
1342
+
1343
+ period_hierarchy:
1344
+ - increment
1345
+ - sprint
1346
+
1347
+ period_types:
1348
+ sprint:
1349
+ label: Sprint
1350
+ attributes:
1351
+ goal:
1352
+ type: string
1353
+ committed_points:
1354
+ type: int
1355
+ body: |
1356
+ ## Sprint Goal
1357
+
1358
+ # --- team roster (optional, same shape) ---
1359
+ resource_prefix: RS # resource ids: RS-1, RS-2, ... distinct from the others
1360
+
1361
+ resource_hierarchy:
1362
+ - [person, role] # one level is usually enough
1363
+
1364
+ resource_types:
1365
+ person:
1366
+ label: Person
1367
+ attributes:
1368
+ email:
1369
+ type: string
1370
+ role:
1371
+ label: Role
1372
+ generic: true # a pool, not a named person
1373
+ attributes:
1374
+ discipline:
1375
+ type: string
1376
+
1377
+ # --- squads (optional, same shape) ---
1378
+ squad_prefix: SP # squad ids: SP-1, SP-2, ...
1379
+ squad_types:
1380
+ squad:
1381
+ label: Squad
1382
+ attributes: {}
1383
+ squad_hierarchy:
1384
+ - squad
1385
+ ```
1386
+
1387
+ `hierarchy` is the single source of truth for parenting: a type's position in
1388
+ the list is the folder depth it must sit at, so `lpm new` and `lpm move` can
1389
+ reject invalid nesting without you declaring parent/child rules twice.
1390
+ `period_hierarchy` and `resource_hierarchy` work identically.
1391
+
1392
+ `priority_attribute` and `effort_attribute` name issue attributes the engine
1393
+ itself reads — the first to order `lpm task next`, the second to add up load in
1394
+ `lpm team`. Both are optional, and both must name an attribute your issue types
1395
+ actually declare, with a usable type (an enum, and an int or float).
1396
+
1397
+ `body` is the markdown scaffolding written into each new document of that type —
1398
+ this is where the Agile practice lives (story format, acceptance criteria,
1399
+ definition of done, repro steps, sprint goal, retro prompts).
1400
+
1401
+ ### The unit of work
1402
+
1403
+ `atomic: true` on an issue type says that work of that type is **taken whole**.
1404
+ It is the answer to "what is one job for one person?", and it is the only thing
1405
+ that decides what the queue offers.
1406
+
1407
+ Without it, only an issue with nothing nested inside it carries work. That reads
1408
+ well until somebody breaks a story into sub-tasks: the story disappears from
1409
+ `lpm task next` and three sub-tasks appear in its place, as if they were three
1410
+ separate tickets for three separate people. Usually they are not — they are a
1411
+ checklist, and the story is still the job.
1412
+
1413
+ So a type marked `atomic` is offered **even when it has children**, and nothing
1414
+ nested inside it is offered separately:
1415
+
1416
+ ```
1417
+ epic container — not offered
1418
+ feature container — not offered
1419
+ user_story ** OFFERED (atomic)
1420
+ sub_task inside the unit — never offered
1421
+ sub_task inside the unit — never offered
1422
+ ```
1423
+
1424
+ The sub-tasks are not hidden, only un-assignable on their own: they are listed
1425
+ in the brief `lpm instructions` renders, so whoever picks the story up reads them
1426
+ with it. `lpm team` counts the same way — the story's `story_points` once, and
1427
+ not the estimates on the sub-tasks underneath.
1428
+
1429
+ Where atomic types nest, the outermost one wins: mark `feature` as well and the
1430
+ feature becomes the unit, with its stories inside it. A board that marks nothing
1431
+ behaves exactly as it always has, so this changes nothing on an existing board
1432
+ until you ask for it. The shipped `scrum` template marks the whole delivery level
1433
+ (`user_story`, `bug`, `test`, `review`, `research`) and `kanban` marks `story`.
1434
+
1435
+ The flag is only meaningful on issue types; `lpm check` refuses it on a period or
1436
+ resource type rather than ignoring it.
1437
+
1438
+ ### Attribute types
1439
+
1440
+ | Type | Frontmatter value | `--set` input |
1441
+ | --- | --- | --- |
1442
+ | `string` | text on one line | `--set owner=jane` |
1443
+ | `text` | multi-line text | `--set notes="..."` |
1444
+ | `int` | integer | `--set story_points=3` |
1445
+ | `float` | number | `--set estimate_hours=1.5` |
1446
+ | `bool` | `true` / `false` | `--set blocked=yes` |
1447
+ | `date` | `YYYY-MM-DD` | `--set due=2026-09-30` |
1448
+ | `enum` | one of `values` | `--set priority=high` |
1449
+ | `array` | YAML list | `--set labels=web,api` |
1450
+
1451
+ Every attribute also accepts `description`, `required: true`, and `default`.
1452
+ Names must be `lower_snake_case` and cannot shadow the reserved fields:
1453
+
1454
+ - **issues** — `id`, `type`, `title`, `status`, `assignee`, `period`, `flag`, `depends_on`, `relates_to`, `related_files`, `created`, `updated`, `author`
1455
+ - **periods** — `id`, `type`, `title`, `starts`, `ends`, `active`, `created`, `updated`, `author`
1456
+ - **resources** — `id`, `type`, `title`, `capacity`, `covers`, `created`, `updated`, `author`
1457
+
1458
+ ### Templates
1459
+
1460
+ | Template | Issues | Periods | Resources |
1461
+ | --- | --- | --- | --- |
1462
+ | `scrum` *(default)* | Program › Epic › Feature › User Story ∥ Bug ∥ Test ∥ Review ∥ Research › Sub-task | Increment › Sprint | Person ∥ Role |
1463
+ | `kanban` | Epic › Story › Task | Cycle | Person ∥ Role |
1464
+ | `blank` | Task | — | — |
1465
+
1466
+ ```bash
1467
+ lpm init --template kanban
1468
+ lpm init --template ./my-process.yml # your own
1469
+ ```
1470
+
1471
+ ## Sharing the board through git
1472
+
1473
+ A board lives in `.lpm`, which is a git repository of its own. **Git sync**
1474
+ makes git the board's remote. Every change made from the CLI, the web UI or an
1475
+ agent pulls the latest board first, then commits and pushes it as one commit
1476
+ (`lpm: claim LP-12`). A team, or a swarm of agents on several machines, can
1477
+ then work one board, and nobody runs `git` by hand.
1478
+
1479
+ ```bash
1480
+ lpm git setup # this project's repository, on its own branch _lpm_board_remote
1481
+ lpm git setup --url https://dev.azure.com/acme/plan/_git/board
1482
+ lpm git join # a teammate: clone the shared board into this project
1483
+ lpm git # where it stands; `lpm git sync` to sync now
1484
+ ```
1485
+
1486
+ A change that collides with one somebody else pushed first is **refused, and
1487
+ nothing is written**, so two people cannot both claim one issue: the second is
1488
+ told, and asking again says who holds it. Changes to different documents both
1489
+ land. Any host works (GitHub, GitLab, Bitbucket, Azure DevOps, or any server
1490
+ git can push to), with the credentials git already uses for your code.
1491
+ light-plan stores none, and never waits on a prompt. With the board on the
1492
+ project's own repository, the board branch shares no commit with the code, so
1493
+ the two histories never mix. A board syncs through git or mirrors onto a
1494
+ tracker (below), never both. Swapping one for the other keeps everything:
1495
+ `lpm git setup --turn-off-remotes` turns the trackers off (not removed), and
1496
+ `lpm git off --turn-on-remotes` turns them back on where they left off.
1497
+
1498
+ The full guide covers conflicts, offline work (`LPM_GIT_OFFLINE=1`), turning
1499
+ it off and the design: [`docs/git-sync.md`](docs/git-sync.md). For a step-by-step
1500
+ walkthrough, CLI and web UI, see [`docs/git-sync-tutorial.md`](docs/git-sync-tutorial.md).
1501
+
1502
+ ## Remote boards
1503
+
1504
+ A **remote** is a mirror: light-plan keeps an external tracker — GitHub Issues,
1505
+ Jira Cloud or Linear — in step with a `.lpm` board, in either direction. The
1506
+ board stays the source of truth in your repo; the remote is a reflection of it,
1507
+ in a tool the rest of the team already has.
1508
+
1509
+ **It is git, and it is not git.** The mental model is a git remote: you push
1510
+ your branch out and pull other people's work back, and the `.lpm` folder is the
1511
+ working tree. That analogy is load-bearing, so be clear-eyed about where it
1512
+ stops. Git syncs *files* and knows nothing about what is in them, and when two
1513
+ sides disagree it hands you the conflict together with a merge base you can
1514
+ inspect and resolve by hand. A remote syncs *issues*, each with its own id, its
1515
+ own workflow and its own rules about what a parent, a status or a label may be —
1516
+ and there is **no merge base you can inspect**. The remote may renumber your
1517
+ issues, rewrite their markdown, reorder their labels or silently refuse half a
1518
+ write, and the only way light-plan can tell your edit from theirs is the
1519
+ snapshot it recorded the last time the two sides agreed. That is why the sync
1520
+ keeps a link store and a base snapshot per document, why a field the remote
1521
+ cannot hold is *encoded* rather than dropped in silence, and why the first
1522
+ write asks once — each is an answer to a question git never had to ask. The
1523
+ full guide — the files a remote touches, every mapping block, credentials,
1524
+ people, sprints and what happens to a board deeper than the platform — is
1525
+ [docs/remotes.md](docs/remotes.md); the reasoning is recorded in
1526
+ [docs/remote-sync.md](docs/remote-sync.md), and the per-platform audit is
1527
+ [docs/remote-capabilities.md](docs/remote-capabilities.md).
1528
+
1529
+ A remote lives in `.lpm/config.yml` under `remotes:`. Each entry names a
1530
+ provider plus that provider's own `connection` and `mapping` blocks. Set one up
1531
+ without hand-writing the YAML — `lpm remote add <name> --provider <provider>
1532
+ --<key> <value>…` validates the connection against the provider's own schema and
1533
+ writes the entry in place; `lpm remote` lists what is configured and
1534
+ `lpm remote rm <name>` removes an entry (the per-remote state is kept unless
1535
+ `--purge`).
1536
+
1537
+ **`lpm remote add` drafts the `mapping:` for you** — you do not write one by
1538
+ hand, and you should not need to edit one. It reads this board's own types,
1539
+ statuses and attributes and matches them against what the provider can hold
1540
+ ([src/remote/scaffold.ts](src/remote/scaffold.ts)). Where the words are the
1541
+ board's own, it uses them. Where the words belong to the *platform* — a Jira
1542
+ issue type, a Linear workflow state — it uses **that platform's conventional
1543
+ names**, which the provider states for itself
1544
+ ([src/remote/vocabulary.ts](src/remote/vocabulary.ts)): Jira's `Epic` / `Story` /
1545
+ `Task` / `Bug` / `Sub-task` and `To Do → In Progress → Done`, a Linear team's
1546
+ `Backlog` / `Todo` / `In Progress` / `In Review` / `Done`.
1547
+
1548
+ **Then the connect wizard turns the convention into your project's own words.**
1549
+ It checks the remote is reachable, and asks it what its types and statuses
1550
+ *actually* are — correcting a name your project spells differently, and asking
1551
+ you about any it does not have at all, with the remote's own names as the
1552
+ options. Read-only on the remote. So first-time setup is **one command and one
1553
+ secret**:
1554
+
1555
+ ```bash
1556
+ lpm remote connect # asks which tracker, where it is, and for the credential
1557
+ lpm remote push --all # files the plan; the first write asks once
1558
+ ```
1559
+
1560
+ Nothing about `connect` is required on the command line — it asks — but every
1561
+ question is also a flag, so a scripted run asks nothing:
1562
+
1563
+ ```bash
1564
+ lpm remote connect jira --site https://acme.atlassian.net --project PAY
1565
+ ```
1566
+
1567
+ `connect` is a wizard over three commands that are still there for a script or
1568
+ a CI job, which have nobody to answer a question — and any question it asks can
1569
+ be given as a flag instead, so a fully flagged run is non-interactive too:
1570
+
1571
+ ```bash
1572
+ lpm remote add jira --provider jira --site https://acme.atlassian.net --project PAY
1573
+ echo <api-token> | lpm remote login jira # or JIRA_API_TOKEN in the environment
1574
+ lpm remote setup jira # match the mapping to the live project
1575
+ ```
1576
+
1577
+ A convention that is wrong is never filed blind: the push preflight validates
1578
+ every mapped name against the live project and refuses the push. A `TODO:` line
1579
+ is what is left where a provider states no convention at all, and the remote
1580
+ refuses to open until it is answered — none of the shipped providers leaves one
1581
+ for any of the shipped board templates.
1582
+
1583
+ A connection value light-plan owns is filled in too: `jsonfile`'s tracker lands
1584
+ in `.lpm/remotes/<name>/tracker.json` unless `--file` says otherwise, so the
1585
+ whole of its setup is one command with no account at all:
1586
+
1587
+ ```bash
1588
+ lpm remote connect jsonfile # path, mapping and all
1589
+ lpm remote push --yes # …and it syncs
1590
+ ```
1591
+
1592
+ ```yaml
1593
+ remotes:
1594
+ jira:
1595
+ provider: jira
1596
+ scope: LP-10 # optional; omit to mirror the whole board
1597
+ direction: both # push | pull | both
1598
+ on_delete: unlink # unlink | close | delete — what a deletion does upstream
1599
+ conflict: manual
1600
+ comments: push # push | both — pull remote comments only when both
1601
+ connection:
1602
+ site: https://acme.atlassian.net # your Cloud site
1603
+ project: PAY # the project key issues file into
1604
+ email: ${JIRA_EMAIL} # where the secret comes from, never the secret
1605
+ token: ${JIRA_API_TOKEN}
1606
+ mapping:
1607
+ types: { user_story: { remote: Story } }
1608
+ statuses: { backlog: "To Do", in_progress: "In Progress", done: Done }
1609
+ ```
1610
+
1611
+ `connection` is where and how — GitHub takes `repo: owner/repo`, Jira `site` +
1612
+ `project`, Linear `team`. `mapping` is the board's vocabulary renamed into the
1613
+ remote's: which board type becomes which remote type, which status becomes which
1614
+ remote status, which attribute travels in which field or label.
1615
+
1616
+ `types` and `statuses` both name their counterpart under `remote:`, and both
1617
+ accept the shorthand — `user_story: Story` and `done: Done` are the same
1618
+ declarations written short. You never say *where* a name lands: whether a type
1619
+ rides a native issue-type field or a label is a property of the platform, and
1620
+ the provider already knows it.
1621
+
1622
+ They differ in one way, because the world does. **A type names one remote
1623
+ type** — there is one issuetype to file under. **A status may name several**,
1624
+ because a remote often has more than one word for the same thing:
1625
+
1626
+ ```yaml
1627
+ statuses:
1628
+ done: { remote: [Done, "Won't Fix", Duplicate], push: Done, closed: true }
1629
+ ```
1630
+
1631
+ All three pull back as the board's `done`; `push:` says which one a push
1632
+ writes. Reach for the list on the day you need it — the mapping `lpm remote
1633
+ add` drafts names one state per status.
1634
+
1635
+ What the remote cannot hold degrades down a ladder — **native → custom field →
1636
+ label → a managed block in the body → a comment** — and a field that is
1637
+ `required` with none of those available is **refused** (the sync stops) rather
1638
+ than silently dropped. The table below states, per provider, which rung each
1639
+ construct lands on.
1640
+
1641
+ **Credentials never sit in the committed config.** `email` and `token` name
1642
+ *where* the value comes from and resolve through a chain at sync time: a
1643
+ `${VAR}` reference into the environment, then `.lpm/credentials.json` (written
1644
+ by `lpm remote login`, git-ignored and owner-only), then the platform's own
1645
+ env var (`JIRA_EMAIL`, `JIRA_API_TOKEN`). A literal secret in `config.yml` is
1646
+ refused. `lpm remote add` and `lpm remote setup` both print which credentials
1647
+ are still missing, where to create each one and the command that stores it, so
1648
+ the page to open is never something to go looking for. At a terminal `lpm remote
1649
+ login <name>` **asks** for each key that provider declares — Jira's email and
1650
+ its API token in the one command — with echo off and a bare Enter keeping a
1651
+ value that already resolves; with no terminal it reads one value from stdin, so
1652
+ a script pipes it in. Either way the value is never a flag, and it lands under
1653
+ the provider's own secret key — `api_key` for Linear, `token` for GitHub, and
1654
+ `--key` names one of several. The Basic Auth header is built by the wrapped client (`jira.js`, which is
1655
+ not installed with light-plan — see [the Jira page](docs/remote-jira.md)) — never by hand, and
1656
+ the resolved values are redacted wherever this run prints anything.
1657
+
1658
+ ### Commands
1659
+
1660
+ ```bash
1661
+ lpm remote # list the declared remotes and their state
1662
+ lpm remote connect [<provider>] [--<key> <value>…] # declare + credential + match the mapping
1663
+ lpm remote push [<id>...] # file these documents — the ledger knows where they go
1664
+ lpm remote pull [<key>...] [--parent <id>] # bring remote issues onto the board
1665
+ lpm remote ledger [<name>] [--unlinked] # which remote holds each document
1666
+ lpm remote add <name> --provider <p> --<key> <value>…
1667
+ lpm remote setup [<name>] # credential + reachability + match the mapping to the remote
1668
+ lpm remote rm <name> [--purge]
1669
+ lpm remote off [<name>…] # turn mirrors off, keeping links, mapping and credentials
1670
+ lpm remote on <name> # turn one back on, carrying on from its last sync
1671
+ lpm remote login <name> [--key <k>] # store a credential (asks, or reads stdin — never a flag)
1672
+ lpm remote push [<id>…|--all] [--dry-run] [--limit N] [--yes] # an issue or a period
1673
+ lpm remote pull [<name>] [--dry-run] [--changed]
1674
+ lpm remote sync [<name>] # pull, then push — the git pull --rebase && git push order
1675
+ lpm remote status [<name>] [--local] [--changed] # drift per document, and what arrived upstream
1676
+ lpm remote log [<name>] # the audit trail — every applied sync, most recent first
1677
+ lpm remote resolve <id> --local|--remote # settle a conflict; the next sync applies it
1678
+ lpm remote link <name> <id> <key> # adopt an existing remote issue into a document
1679
+ lpm remote decouple <id> # drop the link and never re-file; the twin is left alone
1680
+ ```
1681
+
1682
+ `push` and `sync` take `--dry-run` to render the plan without writing, `--limit
1683
+ N` to cap the write operations in a run, and `--yes` to confirm
1684
+ non-interactively (CI, an agent). `sync` pulls first, so remote edits merge
1685
+ before local ones are written over them.
1686
+
1687
+ `status` exits 0 in sync, 1 drifted, 2 conflicted, so CI can fail a branch that
1688
+ left the tracker behind. It reports six buckets, and one of them is not about a
1689
+ document on this board at all: **`incoming`** is an issue in the tracker with no
1690
+ document here yet — a story somebody added upstream — named by its remote key
1691
+ and the local document a pull would file it under. `--local` skips the remote
1692
+ half entirely (no credential, no request); `--changed` narrows the read to what
1693
+ moved since the last sync, which cannot tell you what is *absent*, so `incoming`
1694
+ stands down for it.
1695
+
1696
+ **A push creates the vocabulary it needs** — labels a GitHub repository does not
1697
+ define, Projects v2 fields a status needs — and reports each one. None of that
1698
+ is a decision: it is implied by the mapping the board already wrote down, which
1699
+ is why it is not a command of its own.
1700
+
1701
+ **A sprint is not vocabulary; it is a document on the board.** So a push of work
1702
+ never files the timeline: `lpm remote push TL-3` files that sprint, `--all`
1703
+ files the timeline with everything else, and an issue scheduled into a sprint
1704
+ the tracker has not got is filed *unscheduled* — the push that files the sprint
1705
+ writes the assignment, and picks up everything already filed into it. The
1706
+ board's plan and the board's calendar move at different speeds, and requiring
1707
+ the calendar first made it a precondition for filing a single story.
1708
+
1709
+ Any subcommand may be written with the remote's name first — `lpm remote jira
1710
+ push LP-12` — which is how most people say it out loud.
1711
+
1712
+ **Pushing part of a plan is expected.** `lpm remote push LP-12` files the
1713
+ documents you name, asks whether to file the work inside them, and says what it
1714
+ cannot carry yet: a parent that is not upstream (the document files at the top
1715
+ level) and a dependency whose other end is missing (the edge is left off).
1716
+ Neither is permanent — push the parent later and the document moves under it in
1717
+ that same run, push the other end and the edge is written. **A document is
1718
+ mirrored by one remote at a time**, so a second twin is never created: a
1719
+ targeted push refuses and names the holder, a whole-board push reports it as
1720
+ skipped. `lpm remote ledger` is the record, derived from the link stores
1721
+ themselves, and `lpm remote decouple <id>` releases a document so another
1722
+ tracker may take it.
1723
+
1724
+ ### A worked example
1725
+
1726
+ Mirror the `LP-10` feature into a GitHub repository:
1727
+
1728
+ ```bash
1729
+ lpm remote connect github --repo acme/payments --scope LP-10 --name upstream
1730
+ # declares it, asks for the token, checks reachability and the mapping
1731
+ lpm remote push --dry-run # read the plan, and what it would create first
1732
+ lpm remote push --all # first write asks once; then the plan lands
1733
+ lpm remote push LP-12 # or file one document at a time, later
1734
+ lpm remote status # 0 when the board and the tracker agree
1735
+ lpm remote ledger # which remote holds each document
1736
+ lpm remote log # the record of every sync, for "who filed these?"
1737
+ ```
1738
+
1739
+ ### What each provider can carry
1740
+
1741
+ The honest version of the question every sync has to answer: *where does each
1742
+ part of the board go?* Four outcomes, from the ladder above; the full matrix,
1743
+ with the platform specifications it was verified against, is
1744
+ [docs/remote-capabilities.md](docs/remote-capabilities.md).
1745
+
1746
+ **native** — the platform has a real place for it · **provisioned** — it can be
1747
+ given one (a push creates it) · **encoded** — it must ride a label or the
1748
+ managed block, and round-trips with that loss · **refused** — a `required` field
1749
+ with no carrier at all: the sync stops rather than dropping it.
1750
+
1751
+ | Board construct | GitHub (Issues + Projects v2) | Jira Cloud | Linear |
1752
+ | --- | --- | --- | --- |
1753
+ | **hierarchy** | native, 1 level (sub-issues); deeper → encoded | native, ~3 levels (Epic → issue → subtask); deeper → encoded | native, 1 level (sub-issues); deeper → encoded |
1754
+ | **issue type** | native where org issue types are on, else encoded (label) | native | encoded (label) |
1755
+ | **status** | native open/closed; richer workflow → provisioned | native (workflow transitions) | native (workflow states) |
1756
+ | **`depends_on`** | encoded (no blocking edge) | native | native |
1757
+ | **`relates_to`** | encoded (no relates edge) | native | native |
1758
+ | **attributes** | provisioned, limited value types | native (extensive) | encoded (no custom fields) |
1759
+ | **effort / priority** | provisioned | native | native, fixed scales |
1760
+ | **periods** | native (milestone) + provisioned (Iteration) | native (sprint) | native (cycle) |
1761
+ | **assignee** | native (user); a generic pool → encoded | native (user); a pool → encoded | native (user); a pool → encoded |
1762
+ | **comments** | native | native | native |
1763
+
1764
+ The structural headline: **Jira is the most capable of the three** — native
1765
+ types, edges, statuses and rich custom fields — while **Linear is the least
1766
+ capable for attributes** (no custom fields at all, so every board attribute
1767
+ rides a label or the managed block) and **GitHub is the least capable for
1768
+ edges** (no blocking or relates edge anywhere, so `depends_on` and `relates_to`
1769
+ are always encoded). A hierarchy deeper than the native depth is encoded on all
1770
+ three. This table is the platform's capability; the verdict for *your* mapping
1771
+ is `lpm remote push --dry-run`, whose preflight reports every value that cannot
1772
+ be mapped and refuses the push rather than dropping the field. (`lpm remote
1773
+ check` is narrower than its name: it lists the mapped **labels** a repository
1774
+ does not define yet, and only for a provider whose connector can list them.)
1775
+
1776
+ **What "refused" and "dropped" mean.** A construct is refused only when it is
1777
+ `required` *and* no rung of the ladder can carry it — with a writable body the
1778
+ managed block almost always catches it, so a refusal is the rare bottom, not the
1779
+ normal case. Off the bottom of the ladder, a field that is *not* `required` and
1780
+ has no carrier is **dropped** with a warning instead. The concrete refusals
1781
+ worth knowing before you commit: GitHub does not offer `on_delete: delete` at
1782
+ all (deletion is GraphQL-only, admin-level and hard), so a deletion there is a
1783
+ close or an unlink, never a delete; Jira refuses Server/Data Center outright
1784
+ (Cloud only) and a status with no reachable transition, naming the statuses that
1785
+ are; Linear's `estimate` refuses a value off the team's configured scale. These
1786
+ are the edges where the remote's own rules win over the board, and they are
1787
+ reported rather than papered over.
1788
+
1789
+ **The first write to a remote asks once, and a large plan stops.** A push that
1790
+ would make the first write to a remote shows the target and the counts and asks
1791
+ for confirmation; the consent is recorded in the remote's link store, so it is
1792
+ asked once. A push whose plan would create or close more than the threshold —
1793
+ 25 by default, settable per remote as `write_threshold` — stops and requires
1794
+ `--yes`, because a scope typo that makes half the board look deleted would
1795
+ otherwise close half a backlog. A run with no terminal to ask (CI, an agent)
1796
+ never prompts: it proceeds with `--yes` or stops with a message. `--limit N`
1797
+ still caps any run at N remote write operations, reporting the rest as
1798
+ deferred.
1799
+
1800
+ **Deleting a local document never deletes the remote issue by default.** When a
1801
+ linked document is removed (`lpm rm`) or moves out of the remote's `scope:`,
1802
+ the remote's `on_delete` policy decides what happens to its twin: `unlink` (the
1803
+ default) leaves the twin alone and drops the link, `close` closes it with a
1804
+ comment saying why, and `delete` removes it — only where the platform allows,
1805
+ and with confirmation every run (a delete never rides a remembered consent).
1806
+
1807
+ **Every sync leaves a record.** Each applied `push`, `pull` or `sync` appends a
1808
+ line to `.lpm/remotes/<name>/log.jsonl` — when it ran, who drove it, the
1809
+ operation counts and each failure's reason, with secrets redacted. The file is
1810
+ append-only and line-delimited, so concurrent runs and git merges both leave it
1811
+ readable. `lpm remote log [<name>]` reads it back, most recent first, with
1812
+ `--since <iso>` to window it and `--json` for a script. A dry run is never
1813
+ logged: the log records what a sync did, not what it previewed.
1814
+
1815
+ **Jira connection** (`provider: jira`) targets Atlassian Cloud over Basic Auth
1816
+ with an API token. `site` is an https URL — `https://<org>.atlassian.net`, or a
1817
+ Cloud site on a custom domain. `project` is the key (`PAY`); `board` is the
1818
+ optional Agile board id that enables sprint mapping. Server/Data Center
1819
+ instances are out of scope and get a clear "not supported" rather than a
1820
+ confusing 401.
1821
+
1822
+ **TLS verification is on and stays on.** A corporate MITM proxy with a custom
1823
+ root CA is handled by injecting that CA into the trust store Node's built-in
1824
+ `fetch` already verifies against — never by disabling verification:
1825
+
1826
+ ```bash
1827
+ export NODE_EXTRA_CA_CERTS=/path/to/your-corporate-root-ca.pem
1828
+ # or, on Node 22.19+:
1829
+ node --use-system-ca dist/cli/index.js …
1830
+ ```
1831
+
1832
+ `connection.tls_verify: false` exists as an explicit per-remote fallback for a
1833
+ proxy that cannot be talked into a trust store. It warns on every run, and no
1834
+ error message suggests it before the trust-store path.
1835
+
1836
+ **Comments sync one way by default, and only widen on request.**
1837
+ `comments: push` (the default) posts new `_comments.md` entries upstream, naming
1838
+ the local author in the body; `comments: both` also appends remote comments to
1839
+ the log on pull, with the remote author and timestamp. Either way the link
1840
+ store records each synced comment's remote id, so nothing is ever posted or
1841
+ appended twice, and a comment edited or deleted upstream is never rewritten
1842
+ locally — the log is append-only. The managed comment (the degraded-field block
1843
+ when `encoding: comment`) is excluded from both directions.
1844
+
1845
+ **A Jira status change is a workflow transition, not a write.** Pushing a card
1846
+ to `in_progress` finds the transition from the issue's current status to the
1847
+ mapped status and executes it. A target with no direct hop is walked over
1848
+ several transitions only when `mapping.transitions.multi_hop: true` is set —
1849
+ off by default, because transitions fire automations, notify people and stamp
1850
+ resolutions — and is otherwise refused with the path it would have taken. An
1851
+ unreachable target is refused naming the statuses that *are* reachable. A
1852
+ transition screen's required fields (a resolution, a reason) are answered from
1853
+ `mapping.transition_fields`, keyed by the Jira field key, and a required field
1854
+ with no entry refuses the move naming the field.
1855
+
1856
+ ```yaml
1857
+ mapping:
1858
+ transitions: { multi_hop: true } # opt in to walking the workflow
1859
+ transition_fields: { resolution: Done } # answers for required screen fields
1860
+ ```
1861
+
1862
+ **Jira addresses assignees by account id, never by email or name** — so how a
1863
+ board's people become assignees depends on which resource attribute
1864
+ `mapping.accounts.via` names:
1865
+
1866
+ ```yaml
1867
+ mapping:
1868
+ accounts: { via: jira_account_id } # the attribute holds the account id
1869
+ # accounts: { via: email } # or: resolve the email by search
1870
+ ```
1871
+
1872
+ `via: jira_account_id` is the robust choice: the attribute holds the Jira
1873
+ account id directly and it is written with no search. `via: email` resolves the
1874
+ person's email to an account id through Jira's user search — and on a
1875
+ privacy-restricted (GDPR-mode) instance that search returns nothing, which the
1876
+ sync refuses with the `jira_account_id` alternative named rather than reporting
1877
+ "user not found". An assignee in Jira who matches nobody on the roster is
1878
+ reported on pull, never auto-created.
1879
+
1880
+ ## The template registry
1881
+
1882
+ > Full reference: [docs/templates.md](docs/templates.md).
1883
+
1884
+ The same shape keeps coming back. Every API feature needs a schema story, an
1885
+ endpoint story and a docs story; every migration goes through the same four
1886
+ steps; the team decided months ago how a release is checked. Writing it out
1887
+ again each time is slow, and each copy comes out slightly different from the
1888
+ last.
1889
+
1890
+ The **registry** (`.lpm/registry/`) is where those go. A template is written in
1891
+ the board's own issue types and nests the same way, so it is the *shape of real
1892
+ work* rather than a description of one:
1893
+
1894
+ ```bash
1895
+ lpm template list # what the registry offers, and what each is for
1896
+ lpm template show TPL-3 # its parameters, and everything it would create
1897
+ lpm template apply TPL-3 --params ./payments.json --under LP-14
1898
+ ```
1899
+
1900
+ ```
1901
+ TPL-3 Feature {{name}} API (+3 documents)
1902
+ Delivery / Epic slot
1903
+ REST endpoint with schema, tests and docs
1904
+ parameters: name, owner
1905
+ ```
1906
+
1907
+ ### Writing one
1908
+
1909
+ ```bash
1910
+ lpm template new folder -t "Delivery" -d "Standard delivery patterns"
1911
+ lpm template new folder -t "Epic slot" --parent TPL-1
1912
+ lpm template new feature -t "{{name}} API" --parent TPL-2 \
1913
+ -d "REST endpoint with schema, tests and docs" \
1914
+ --param name:string:required --param owner:string=nobody
1915
+ lpm template new user_story -t "Design the {{name}} schema" --parent TPL-3
1916
+ lpm template new user_story -t "Implement {{name}} endpoints" --parent TPL-3
1917
+ lpm link TPL-5 --depends-on TPL-4
1918
+ ```
1919
+
1920
+ The registry **mirrors the issue hierarchy**: a template of a feature sits at the
1921
+ feature's depth, exactly where the issue it produces will sit. Something has to
1922
+ occupy the levels above it, and that is what a `folder` is — a container standing
1923
+ in for a level nobody templatized. So to keep a feature template on its own you
1924
+ file it under a folder where the epic would go, and to templatize an epic you put
1925
+ it at the top level beside that folder. A folder is never instantiated, and never
1926
+ sits *inside* a template.
1927
+
1928
+ The template somebody instantiates — the one whose parent is a folder, or
1929
+ nothing — is its **root**. Everything nested under it comes with it, and only the
1930
+ root declares parameters.
1931
+
1932
+ ### Parameters
1933
+
1934
+ A parameter is declared exactly like a board attribute (`type`, `required`,
1935
+ `default`, `values` for an enum), and `{{name}}` is written wherever the answer
1936
+ goes: in titles, in bodies, in `related_files` and in attribute values.
1937
+
1938
+ ```yaml
1939
+ ---
1940
+ id: TPL-3
1941
+ type: feature
1942
+ title: "{{name}} API"
1943
+ description: REST endpoint with schema, tests and docs
1944
+ params:
1945
+ name:
1946
+ type: string
1947
+ required: true
1948
+ description: What the API is for
1949
+ owner:
1950
+ type: string
1951
+ default: nobody
1952
+ ---
1953
+ ```
1954
+
1955
+ Answers arrive as a JSON object, from a file or one at a time:
1956
+
1957
+ ```bash
1958
+ lpm template apply TPL-3 --params ./payments.json --under LP-14
1959
+ lpm template apply TPL-3 --set name=Payments --set owner=Ana --under LP-14
1960
+ lpm template apply TPL-3 --set name=Payments --dry-run
1961
+ ```
1962
+
1963
+ An attribute whose *whole* value is one placeholder keeps the parameter's own
1964
+ type, so `story_points: "{{points}}"` writes the number 8 rather than the string
1965
+ "8" — which is what lets a template carry a value the board would otherwise
1966
+ reject.
1967
+
1968
+ Three things it refuses rather than guessing at:
1969
+
1970
+ - a **required parameter with no answer**, naming it;
1971
+ - an **answer the template never asked for**, because a typo in a parameter file
1972
+ is otherwise a template that quietly produced the wrong board;
1973
+ - a **placeholder no parameter declares**, for the same reason — `{{nmae}}` would
1974
+ otherwise be copied onto the board verbatim. `lpm check` reports that one as a
1975
+ warning on the template itself, before anybody instantiates it.
1976
+
1977
+ And one it refuses out of respect for the hierarchy: a feature template has to
1978
+ land somewhere a feature can sit, so `--under` is checked before anything is
1979
+ written.
1980
+
1981
+ Instantiating creates the whole tree at once, fills in every answer, and
1982
+ **repoints the dependencies between the templates at the issues it just
1983
+ created** — so a feature template with three chained stories lands as three
1984
+ chained stories. Dependencies leaving the template are dropped, exactly as they
1985
+ are when a structure is duplicated: a fresh copy stands on its own. The registry
1986
+ itself is never touched, so the same template can be applied as often as you like.
1987
+
1988
+ ### For agents
1989
+
1990
+ The MCP server carries `list_templates`, `get_template` and
1991
+ `instantiate_template`, and its instructions tell an agent to check the registry
1992
+ *before* creating documents by hand. That is the point of the feature as much as
1993
+ the typing it saves: a template named "Database migration" is the team saying
1994
+ "this is how we do those", and an agent that writes its own four tickets instead
1995
+ has quietly skipped a process somebody wrote down. `list_templates` is cheap and
1996
+ returns enough — the description, the parameters, how much it would create — to
1997
+ decide in one call.
1998
+
1999
+ ### Building one on the canvas
2000
+
2001
+ `lpm ui` → New view → **Template registry**. The same canvas, table and side
2002
+ panel as a board view, over the registry instead: drag templates into folders,
2003
+ draw dependencies between them, edit descriptions and parameters in the panel
2004
+ — every edit reaches the registry on its own within moments, the same as
2005
+ anywhere else in the app. There is nothing new to learn, which is the whole
2006
+ design — building a reusable feature-with-three-stories is the same gesture as
2007
+ building a real one.
2008
+
2009
+ Instantiating stays on the CLI and MCP, where the parameter file lives.
2010
+
2011
+ ### What it is not
2012
+
2013
+ `.lpm/templates/context/` is a different folder doing a different job: those are
2014
+ the layouts `lpm instructions` renders a working brief with, and they *are*
2015
+ executable ([see above](#-context-templates-are-code)). A registry template is a
2016
+ document. `{{name}}` is replaced with a value; there is no logic, nothing is
2017
+ compiled and nothing is evaluated.
2018
+
2019
+ Nothing in the registry gates work, ranks a queue or appears in `lpm task next`.
2020
+ It is a catalogue.
2021
+
2022
+ ## What has to happen first: upstream work
2023
+
2024
+ `lpm task next` answers "what can I pick up **now**", and stops at the first
2025
+ thing standing in the way. `lpm upstream` answers the longer question behind it
2026
+ — everything that would have to happen for one issue to be closed at all:
2027
+
2028
+ ```bash
2029
+ lpm upstream LP-42
2030
+ ```
2031
+
2032
+ ```
2033
+ LP-42 Pay as a guest
2034
+ 3 issues must be finished first
2035
+
2036
+ ← LP-31 Payment gateway (Feature — Backlog)
2037
+ · LP-33 Webhook receiver (User Story — Backlog, Ana Ruiz, Sprint 7)
2038
+ · LP-34 Vault credentials (User Story — Backlog)
2039
+
2040
+ ← something it waits on · open work inside one
2041
+ ```
2042
+
2043
+ Two kinds of line, and the second is the one a list of edges would miss:
2044
+
2045
+ - **`←` something it waits on.** The dependency as it is *written*, inherited
2046
+ from the issues above exactly as the queue inherits it: a story inside a
2047
+ feature waits on whatever the feature waits on.
2048
+ - **`·` open work inside one.** A container is finished when its contents are,
2049
+ so a feature standing in the way is really its unfinished stories standing in
2050
+ the way — and those are what somebody can actually be handed.
2051
+
2052
+ Finished work drops out on both counts, so the list shrinks as the plan
2053
+ progresses rather than having to be maintained.
2054
+
2055
+ Ask it about a **container** and the report ends with what the work inside it
2056
+ puts the container after — the reflection described under
2057
+ [dependencies](#dependencies), naming the written edge each line comes from:
2058
+
2059
+ ```
2060
+ LP-3 Guest flow
2061
+ nothing upstream — everything it waits on is finished
2062
+
2063
+ the work inside it puts it after:
2064
+ ← LP-6 Sign-up (Feature) via LP-4 → LP-7
2065
+ Nothing is written on either container; both stand for the work inside them.
2066
+ ```
2067
+
2068
+ That part is read off the graph and is never touched by `--schedule`: the work
2069
+ it stands for is already reachable through the story that declares it.
2070
+
2071
+ ### Pushing it into the queue
2072
+
2073
+ Seeing the chain and staffing it are the same gesture with one flag:
2074
+
2075
+ ```bash
2076
+ lpm upstream LP-42 --schedule
2077
+ ```
2078
+
2079
+ Every unclaimed piece of upstream work goes into the **same period** as LP-42
2080
+ and to the **same person or pool**, so a chain nobody had scheduled becomes work
2081
+ the queue offers. Two rules, both deliberately narrow:
2082
+
2083
+ - **Work somebody already holds is left alone**, its period included. It is
2084
+ their work, and shuffling it between sprints would be a worse surprise than a
2085
+ short list. It is still reported, so you can see who has it.
2086
+ - **Only work units are scheduled.** A period written on an epic offers nobody
2087
+ anything, because the queue hands out units — so containers are listed and
2088
+ left as they are. It is the same rule dragging a feature into a sprint box
2089
+ follows on the canvas.
2090
+
2091
+ `--dry-run` says what would happen. `--period` and `--assignee` override what is
2092
+ copied, and `--unscheduled` / `--unassigned` turn off one half:
2093
+
2094
+ ```bash
2095
+ lpm upstream LP-42 --schedule --dry-run
2096
+ lpm upstream LP-42 --schedule --assignee RS-2 --period TL-3
2097
+ lpm upstream LP-42 --schedule --unscheduled # assign, but do not schedule
2098
+ ```
2099
+
2100
+ Both halves are on the canvas as well — right-click an issue for **Add upstream
2101
+ dependencies** (draws the whole chain, changing nothing) and **Schedule upstream
2102
+ dependencies** (the same edit, queued for the next Push) — and on the MCP server
2103
+ as `upstream_work` and `schedule_upstream`. All three read the one definition in
2104
+ `src/shared/blocking.ts`, so they cannot tell different stories.
2105
+
2106
+ ## Reshaping the plan
2107
+
2108
+ Plans do not survive contact with the work. Four commands do the rewiring that
2109
+ makes changing one painless — the same operations the web UI offers on a
2110
+ right-click, so a board reshaped from the terminal and one reshaped by dragging
2111
+ nodes come out identical.
2112
+
2113
+ **Break an issue up.** A 12-point story is a guess, not a plan:
2114
+
2115
+ ```bash
2116
+ lpm split LP-7 --into 3 --replace --split-effort
2117
+ lpm split LP-7 --titles "Schema,API,UI" --replace
2118
+ lpm split LP-7 --into 5 # as children, keeping LP-7
2119
+ ```
2120
+
2121
+ `--replace` puts the pieces where the original stood and deletes it;
2122
+ `--children` (the default) nests them inside it. Either way the graph is kept
2123
+ intact: **whatever blocked the original blocks the first piece, and whatever
2124
+ waited on it waits on the last**, with the pieces chained in between.
2125
+ `--split-effort` divides the board's effort attribute across them, and skips
2126
+ quietly when the pieces are measured in something else.
2127
+
2128
+ **Change what something is.** A type that belongs at another depth takes the
2129
+ document with it:
2130
+
2131
+ ```bash
2132
+ lpm convert LP-7 bug # same level, different type
2133
+ lpm convert LP-7 feature # promoted, and moved up to its epic
2134
+ lpm convert LP-7 --under LP-9 # demoted to fit under LP-9
2135
+ lpm convert LP-7 --under LP-1 --build-parents
2136
+ ```
2137
+
2138
+ The third form is the command-line spelling of dropping a node onto another in
2139
+ the canvas: it works out what type LP-7 has to become to live under LP-9, and
2140
+ refuses if anything nested under it would end up with nowhere to sit.
2141
+
2142
+ A move that skips levels has a second answer, and `--build-parents` is it: a
2143
+ story dropped onto a program either becomes an epic, or *keeps its type* and
2144
+ gets the epic and feature it was missing, created on the way. Nothing nested
2145
+ under it changes at all, which is the reason to prefer it.
2146
+
2147
+ **Insert a step into a dependency.** `a -> b` becomes `a -> new -> b`, replacing
2148
+ the edge rather than adding to it:
2149
+
2150
+ ```bash
2151
+ lpm insert --between LP-4..LP-7 -t "Validate the payload"
2152
+ lpm insert --between LP-4..LP-7 --issue LP-9 # move an existing issue in
2153
+ ```
2154
+
2155
+ **Duplicate a structure.** Dependencies between the copied documents are kept
2156
+ and repointed at the copies; those leaving the selection are dropped, so the
2157
+ duplicate stands on its own:
2158
+
2159
+ ```bash
2160
+ lpm copy LP-3 # the feature and its stories
2161
+ lpm copy LP-3 --under LP-9
2162
+ ```
2163
+
2164
+ Every one of these takes `--dry-run`, and every one refuses a change that would
2165
+ break the hierarchy or close a dependency cycle before writing anything.
2166
+
2167
+ ## Comments
2168
+
2169
+ Documents say what the work *is*. Comments say how it went:
2170
+
2171
+ ```bash
2172
+ lpm comment LP-7 "Blocked on the sandbox credentials; asked infra"
2173
+ lpm comment LP-7 --file ./run-notes.md
2174
+ lpm comment LP-7 --list
2175
+ lpm comment LP-7 --remove 2
2176
+ ```
2177
+
2178
+ They live in a `_comments.md` beside the document, as a flat append-only list:
2179
+
2180
+ ```markdown
2181
+ ## 2026-08-02T09:14:02.104Z — Alice Smith (RS-1)
2182
+
2183
+ Tried the naive join first; too slow at 100k rows.
2184
+
2185
+ ## 2026-08-02T11:40:55.881Z — a jr. developer
2186
+
2187
+ Added an index on `created`. 40ms now.
2188
+ ```
2189
+
2190
+ That shape is chosen for what actually happens to comments: someone appends one,
2191
+ and two people do it on different branches. Appends merge; a growing list in the
2192
+ frontmatter would not, and it would push the fields tools read down the page.
2193
+ The engine never loads them — `lpm check` does not care what you wrote — so the
2194
+ log can be as long as the work deserves.
2195
+
2196
+ The author is whoever `lpm me` says you are, and the side panel in the web UI
2197
+ writes to the same file.
2198
+
2199
+ ## Working with agents
2200
+
2201
+ `lpm mcp` serves the board over the [Model Context
2202
+ Protocol](https://modelcontextprotocol.io), so an agent can use light-plan the
2203
+ way a person does — the same operations, the same rules, the same files.
2204
+
2205
+ `lpm mcp setup` writes the host configuration for you:
2206
+
2207
+ ```bash
2208
+ lpm mcp setup # -> <board>/.mcp.json
2209
+ lpm mcp setup --user "Planner Bot" # the agent acts as this team member
2210
+ lpm mcp setup --file ~/.cursor/mcp.json # merge into an existing config
2211
+ lpm mcp setup --print # just show it
2212
+ ```
2213
+
2214
+ ```jsonc
2215
+ {
2216
+ "mcpServers": {
2217
+ "light-plan": {
2218
+ "command": "lpm",
2219
+ "args": ["mcp", "--user", "Planner Bot"],
2220
+ "cwd": "/path/to/your/project"
2221
+ }
2222
+ }
2223
+ }
2224
+ ```
2225
+
2226
+ With `--file` the entry is merged in place: the rest of the file is untouched,
2227
+ and the key is matched to the one it already uses — `servers` for VS Code,
2228
+ `mcpServers` for Claude Code, Claude Desktop and Cursor. Without it a new file
2229
+ is written, and an existing one is never clobbered unless you pass `--force`.
2230
+ Add a second, differently-named entry with `--name`:
2231
+
2232
+ ```bash
2233
+ lpm mcp setup --file .mcp.json --name light-plan-ro --read-only
2234
+ ```
2235
+
2236
+ The tools come in three groups. **Reading** — `board_overview` (call it first:
2237
+ boards define their own types and statuses, and every other tool speaks in
2238
+ those names), `list_documents`, `get_document`, `get_instructions`,
2239
+ `check_board`. **Planning** — `create_document`, `update_document`,
2240
+ `convert_document`, `split_issue`, `insert_between`, `copy_documents`,
2241
+ `delete_document`, `link_issues`.
2242
+ **Working** — `next_tasks`, `current_tasks`, `start_task`, `finish_task`,
2243
+ `flag_issue`, `clear_flag`, `flagged_issues`, `add_comment`, `list_comments`,
2244
+ `team_load`.
2245
+ **Remote** — `remote_status` and `remote_preview` report the drift between the
2246
+ board and its tracker, and what a sync would do, without writing anything;
2247
+ `remote_sync` is the one tool that writes to a system outside the checkout and
2248
+ is registered only under `--allow-remote` (see below).
2249
+
2250
+ `get_document` and `get_instructions` are the pair worth telling apart.
2251
+ `get_document` returns one document as a record — fields, attributes, links,
2252
+ ancestors as ids — and is what an agent wants before *changing* something.
2253
+ `get_instructions` returns [the working brief](#working-briefs-the-context-to-actually-do-it):
2254
+ the same document with the titles and bodies of its whole ancestry laid out above
2255
+ it, as markdown to work from. An agent that reads only the story builds the right
2256
+ code for the wrong reason, which is why the shipped `lpm-developer` agent calls
2257
+ it before writing anything.
2258
+
2259
+ The reshaping tools go through the same planners as the CLI and the canvas, so
2260
+ an agent that splits a story gets exactly the rewiring a person would.
2261
+
2262
+ This is meant for a mixed team. One agent writes the plan; others pick work up
2263
+ with `next_tasks`, record what they tried with `add_comment`, and close it with
2264
+ `finish_task` — while a human watches the same board in `lpm ui` and a reviewer
2265
+ comments on the same issues. Because dependencies gate what `next_tasks` offers,
2266
+ finishing one issue is what hands the next to whoever asks first.
2267
+
2268
+ `flag_issue` is the tool for the case that otherwise goes badly. An agent that
2269
+ cannot finish something has three bad options — guess at the requirement, build a
2270
+ workaround nobody asked for, or drift onto another issue leaving this one open —
2271
+ and one good one, which is to say so and stop. A flag keeps the issue assigned,
2272
+ turns it red for the human reading the board, and forces the comment that makes
2273
+ it actionable. `clear_flag` is deliberately described as the plan owner's tool:
2274
+ an agent should call it when it is the one answering somebody else's flag, not to
2275
+ get past its own.
2276
+
2277
+ Three flags matter for that:
2278
+
2279
+ - `--user <id|name>` is **per session and never written to `.lpm/local.json`**,
2280
+ so a dozen agents can share one checkout without overwriting each other's
2281
+ answer to "who am I". It sets who work is assigned to and who comments are
2282
+ signed by.
2283
+ - `--profile <file>` points the session at a [profile](#profiles-giving-one-developer-one-part-of-the-board),
2284
+ which is how five agents get five slices of one plan. It can carry the
2285
+ identity too, so one file per agent is the whole setup:
2286
+
2287
+ ```bash
2288
+ lpm mcp setup --name frontend --profile ./profiles/frontend-agent.yml --file .mcp.json
2289
+ ```
2290
+
2291
+ `board_overview` reports the scope in force and anything wrong with the file,
2292
+ and `list_documents` says how many issues it left out — an agent is told what
2293
+ it is not being shown rather than left to wonder why the board looks small.
2294
+ As on the CLI, the scope narrows `next_tasks` and `list_documents` and nothing
2295
+ else: `get_document` still reads any id, because an agent handed a dependency
2296
+ it cannot open is worse off than one shown work it should leave alone.
2297
+ - `--read-only` registers only the tools that do not write, for an agent that
2298
+ should report on the plan rather than change it.
2299
+ - `--allow-remote` registers `remote_sync`, the one tool that writes to a
2300
+ tracker outside the checkout. `remote_status` and `remote_preview` are always
2301
+ registered; `remote_sync` is not registered at all without the flag — absent,
2302
+ not present-and-failing, so a model does not keep retrying it. `--read-only`
2303
+ excludes every writing tool, remote included, regardless of `--allow-remote`.
2304
+ The remote's credentials are the *board's*, not the agent's: every agent that
2305
+ runs `remote_sync` pushes as the same tracker account.
2306
+
2307
+ The SDK is an optional dependency, so it installs by default but the engine
2308
+ itself keeps its four. If you installed with `--no-optional`, `lpm mcp` says what
2309
+ to install.
2310
+
2311
+ ### Ready-made agents and skills
2312
+
2313
+ The package ships two agent definitions and five skills in [`assets/`](assets),
2314
+ and `lpm agent` installs them into a project in whatever layout your harness
2315
+ expects:
2316
+
2317
+ ```bash
2318
+ lpm agent --list # what ships, and where it lands
2319
+ lpm agent --target claude # asks project or user account
2320
+ lpm agent --target copilot --type developer --project
2321
+ lpm agent --target reasonix --global
2322
+ lpm agent --target claude --dry-run # say what would happen
2323
+ ```
2324
+
2325
+ | `--target` | Agents | Skills | MCP |
2326
+ | --- | --- | --- | --- |
2327
+ | `claude` | `.claude/agents/*.md` | `.claude/skills/*/SKILL.md` | `.mcp.json` |
2328
+ | `copilot` | `.github/agents/*.agent.md` | `.github/skills/*/SKILL.md` | `.mcp.json` |
2329
+ | `reasonix` | `.reasonix/commands/*.md` (`runAs: subagent`) | `.reasonix/commands/*.md` | `.mcp.json` |
2330
+
2331
+ All three read a project `.mcp.json` in the standard format, so a project set up
2332
+ for all of them ends up with one MCP config rather than three. User-level
2333
+ installs differ more: `~/.claude/` + `~/.claude.json`, `~/.copilot/` +
2334
+ `~/.copilot/mcp-config.json`, and `~/.reasonix/commands/` — where the MCP entry
2335
+ is printed rather than written, because Reasonix keeps user servers in TOML.
2336
+ [`docs/harness-layouts.md`](docs/harness-layouts.md) records every path, where it
2337
+ came from, and what to check when adding a target.
2338
+
2339
+ `--type developer` installs the agent that picks work up, implements it and
2340
+ leaves a reviewable trail on the issue, plus the skills it needs; `--type pm`
2341
+ installs the one that writes epics, features and stories and sequences them.
2342
+ Without `--type` you get both. `--global` installs for your user account instead
2343
+ of the project, and with neither `--project` nor `--global` you are asked.
2344
+
2345
+ **An install never destroys what was already there.** An existing directory is
2346
+ added to, an existing MCP config is merged into with its other servers and keys
2347
+ untouched, and a file the harness owns — Copilot's `copilot-instructions.md` —
2348
+ gets a delimited block that is replaced in place on the next run rather than
2349
+ appended twice. A file the command wrote before is only overwritten with
2350
+ `--force`.
2351
+
2352
+ Only the frontmatter differs between targets; the bodies are the same everywhere,
2353
+ because the instructions are about light-plan rather than about who is reading
2354
+ them. Read them as a description of how the tools are meant to be used, whichever
2355
+ host you run.
2356
+
2357
+ The assets are a **neutral tree** and each layout is a **declarative mapping**,
2358
+ so no code knows a harness by name:
2359
+
2360
+ ```
2361
+ assets/
2362
+ agents/<name>.md the canonical assets — any files at all
2363
+ skills/<name>.md
2364
+ harnesses/<name>.yml a list of copy rules, for one harness
2365
+ hcm/<name>.yml the same, for one hcm bundle (lpm hcm init)
2366
+ ```
2367
+
2368
+ A mapping selects source files the way a `.gitignore` does and says where each
2369
+ one goes:
2370
+
2371
+ ```yaml
2372
+ files:
2373
+ - from: skills/*.md # markdown: rewrite the frontmatter
2374
+ to: "{root}/skills/{name}/SKILL.md"
2375
+ frontmatter:
2376
+ name: "{name}"
2377
+ description: "{description}"
2378
+
2379
+ - from: "scripts/**" # no frontmatter: copy byte for byte
2380
+ to: "{root}/scripts/{path}"
2381
+ ```
2382
+
2383
+ So supporting a new host is one YAML file, and shipping a new asset — a skill, a
2384
+ hook script, a config template — is a file plus a pattern that selects it.
2385
+ [`assets/README.md`](assets/README.md) is the short version and
2386
+ [`docs/harness-layouts.md`](docs/harness-layouts.md) the long one, including what
2387
+ each host's layout actually is and where that was verified from.
2388
+
2389
+ ### The same assets as an hcm bundle
2390
+
2391
+ If you manage agent configuration with
2392
+ [hcm](https://www.npmjs.com/package/harness-config-manager), `lpm hcm init`
2393
+ registers light-plan's agents, skills and MCP server as the `light-plan` bundle,
2394
+ and from then on any project installs them the hcm way:
2395
+
2396
+ ```bash
2397
+ lpm hcm init # once, and again after each upgrade
2398
+ hcm install light-plan -t claude-code # in any project
2399
+ hcm install light-plan -t copilot --flavor developer
2400
+ hcm update light-plan # after re-running init
2401
+ ```
2402
+
2403
+ Without a global install, run it from the package — `npx light-plan hcm init`
2404
+ from anywhere, or `npx --no lpm hcm init` in a project that depends on
2405
+ `light-plan` (`--no` is what stops npx fetching the unrelated `lpm` package).
2406
+
2407
+ The bundle is **rendered, not kept**: `init` builds it from the same `assets/`
2408
+ that `lpm agent` installs, at this package's version, into your per-user data
2409
+ folder (`%LOCALAPPDATA%\light-plan\hcm`, `~/Library/Application Support/…`,
2410
+ `~/.local/share/…`), then runs `hcm registry add` on it. Every bundle in
2411
+ `assets/hcm/` is registered by the one command. The two roles are hcm flavors,
2412
+ so `--flavor developer` is `lpm agent --type developer`. `lpm hcm build --dir
2413
+ <path>` renders without registering, for publishing the bundle from a repository;
2414
+ `lpm hcm remove` unregisters it; `lpm hcm init --dev` registers it in place for
2415
+ working on the assets. [`docs/hcm.md`](docs/hcm.md) has the details.
2416
+
2417
+ ## Git strategy
2418
+
2419
+ `.lpm` is initialised as **its own git repository** and added to the surrounding
2420
+ project's `.gitignore`. The board sits inside your code checkout but keeps a
2421
+ separate history and can have its own remote:
2422
+
2423
+ ```bash
2424
+ cd .lpm
2425
+ git add -A && git commit -m "Plan the payments work"
2426
+ git remote add origin git@github.com:you/my-project-board.git
2427
+ git push -u origin main
2428
+ ```
2429
+
2430
+ This was chosen over a submodule deliberately: a submodule needs a commit in the
2431
+ board *plus* a pointer commit in the parent for every change, and contributors
2432
+ have to remember `clone --recursive`. A plain nested repo gives the same
2433
+ independence with none of that ceremony.
2434
+
2435
+ Use `lpm init --no-git` to skip it and manage tracking yourself.
2436
+
2437
+ Because every document is its own file, concurrent edits rarely collide — two
2438
+ people working on different issues never touch the same file, and dependencies
2439
+ only ever write to the file that declares them. The one shared file is
2440
+ `.lpm/state.json` (the id counters); if a merge mangles it, `lpm check --fix`
2441
+ resyncs all three counters from disk, and ids are never reused because
2442
+ allocation skips any id already present.
2443
+
2444
+ `init` also writes a `.lpm/.gitignore` holding `local.json`, the per-checkout
2445
+ file that remembers who you are, and `lock`, the file that exists only while
2446
+ somebody is part-way through a change. Neither is the team's.
2447
+
2448
+ ## Several people and agents, one checkout
2449
+
2450
+ Git covers two people on two machines. This section is the other case: two
2451
+ people, a browser session and a swarm of agents all writing to the *same* `.lpm`
2452
+ folder at the same moment, with no server in front of it. Every one of them is a
2453
+ separate process that read the board, decided something, and is about to write.
2454
+
2455
+ Three rules make that safe, and all three are in the engine, so the CLI, the MCP
2456
+ server, the web app and `lpm queue agent` get them without asking.
2457
+
2458
+ **Nobody ever reads half a document.** Every write — a document, `state.json`,
2459
+ `INDEX.md`, a saved view, `local.json` — goes to a temporary file beside the
2460
+ target and is renamed into place. A reader sees the whole old version or the
2461
+ whole new one; it can never catch a `_issue.md` mid-sentence, which on a YAML
2462
+ frontmatter file reads as a corrupt board.
2463
+
2464
+ **One writer at a time.** Every operation that changes the board takes
2465
+ `.lpm/lock` first — created with `O_EXCL`, so exactly one process can hold it —
2466
+ and gives it up when the operation returns. It is held for the write and never
2467
+ for the work: an agent may spend twenty minutes on a task and holds the lock for
2468
+ the milliseconds it takes to record that it started. If somebody else has it you
2469
+ are told who and what they are doing, rather than left to interleave with them:
2470
+
2471
+ ```
2472
+ error The board is busy: alice is doing "push 14 change(s)" (pid 5512 on lima)
2473
+ Gave up waiting after 10s to claim LP-12.
2474
+ ```
2475
+
2476
+ A process that is interrupted gives the lock up on its way out, including on
2477
+ Ctrl-C, so the ordinary crash costs nothing. A kill nothing can catch does leave
2478
+ the lock behind, and it is then broken for being **old** — two minutes by
2479
+ default. Age is deliberately the only test: "is that process still running?"
2480
+ looks like the obvious shortcut and is not one, because the answer is
2481
+ occasionally wrong on a loaded machine, and a lock broken on a wrong answer lets
2482
+ two writers into the same change. Waiting too long costs a wait; breaking too
2483
+ early costs the guarantee.
2484
+
2485
+ Say what it does not do: the lock is advisory and cannot stop a text editor
2486
+ writing `_issue.md`, and breaking a stale one cannot be made perfectly safe
2487
+ without a filesystem primitive nobody has. That is why there is a third rule
2488
+ rather than two.
2489
+
2490
+ **A stale write is refused, not applied.** Every load records what each document
2491
+ looked like when it read it, and every operation checks that against disk before
2492
+ writing. If somebody else changed the file in between, the operation refuses and
2493
+ nothing is written:
2494
+
2495
+ ```
2496
+ error LP-12 changed on disk while you were working on it
2497
+ Somebody else — a person, an agent or an editor — wrote .lpm/board/LP-12/_issue.md.
2498
+ Nothing was written, so nothing was lost. Read it again and repeat the change.
2499
+ ```
2500
+
2501
+ That is deliberately a refusal rather than a merge: light-plan cannot know
2502
+ whether your title and their status change belong together, and the board is in
2503
+ git, so reading again and repeating the change costs a second. The one operation
2504
+ that does not stop there is [claiming](#claiming-is-atomic) — a queue runner's
2505
+ answer to losing a race is obvious, so it re-reads and reports the winner
2506
+ instead of asking a person.
2507
+
2508
+ Two consequences worth knowing. `lpm new` cannot hand two processes the same id,
2509
+ because the counter is read and written under the lock. And `INDEX.md` is
2510
+ rewritten incrementally for speed, so when another process has moved it on since
2511
+ your board was loaded, the operation reloads and renders the whole thing rather
2512
+ than publishing a table of contents missing their work.
2513
+
2514
+ | Variable | What it does |
2515
+ | --- | --- |
2516
+ | `LPM_LOCK_TIMEOUT_MS` | How long to wait for another writer before giving up (default 10000) |
2517
+ | `LPM_LOCK_STALE_MS` | How old a lock has to be before it is assumed to be a crash (default 120000) |
2518
+ | `LPM_NO_LOCK` | Skip locking entirely |
2519
+
2520
+ `LPM_NO_LOCK=1` is the escape hatch for a filesystem where `O_EXCL` does not
2521
+ mean what it says — some network mounts — on which every command would otherwise
2522
+ fail. It removes the first line of defence and leaves the third: a stale write is
2523
+ still refused.
2524
+
2525
+ ## Commands
2526
+
2527
+ | Command | What it does |
2528
+ | --- | --- |
2529
+ | `lpm init [dir]` | Create a board. `--template`, `--prefix`, `--no-git` |
2530
+ | `lpm new <type> [title]` | Create an issue, period or resource. `-t/--title`, `-p/--parent`, `-s/--status`, `--period`, `--assignee`, `--depends-on`, `--relates-to`, `--related`, `--starts`, `--ends`, `--capacity`, `--covers`, `--set k=v` |
2531
+ | `lpm set <id>` | Edit content. `-t/--title`, `--body`, `--body-file`, `--set k=v`, `--related`, `--unrelated`, `--starts`, `--ends`, `--capacity` |
2532
+ | `lpm move <id>` | `-s/--status <id>`, `-p/--parent <id>\|root`, `--period <id>\|none`, `--assignee <id>\|none` |
2533
+ | `lpm convert <id> <type>` | Change the type, moving it if the type belongs elsewhere. `--under <id>`, `--build-parents`, `--dry-run` |
2534
+ | `lpm link <id>` | `--depends-on <ids>`, `--relates-to <ids>`, `--covers <ids>`, `--remove` |
2535
+ | `lpm insert` | Put an issue inside a dependency. `--between <a>..<b>`, `--issue`, `--type`, `-t/--title` |
2536
+ | `lpm split <id>` | Break an issue up. `--into <n>`, `--titles`, `--replace`, `--children`, `--no-chain`, `--split-effort`, `--dry-run` |
2537
+ | `lpm copy <id>...` | Duplicate documents and their subtrees. `--under <id>`, `--dry-run` |
2538
+ | `lpm rm <id>` | Delete a document. `-r/--recursive`, `--dry-run` |
2539
+ | `lpm comment <id> <text>` | Add to the work log. `-m/--message`, `-f/--file`, `--list`, `--remove <n>`, `--author` |
2540
+ | `lpm flag [<id>]` | Say work has stopped, and why. `clear <id>`, `list`, `-m/--comment`, `-f/--file`, `--reason`, `--author` |
2541
+ | `lpm open <id>` | Open in `$VISUAL`/`$EDITOR`. `--path` prints the path instead. Alias: `lpm edit` |
2542
+ | `lpm me [<id\|name>]` | Show or set who is using this checkout. `--clear`. Alias: `lpm whoami` |
2543
+ | `lpm profile [<file>]` | Use a profile: who you are, and which part of the board is yours. `--file`, `--init`, `--user`, `--clear`, `--force`. Alias: `lpm user` |
2544
+ | `lpm task <sub>` | `next`, `current`, `prev`, `start [id]`, `done [id]`. `--limit`, `--unassigned`, `--parked`, `--force` |
2545
+ | `lpm upstream <id>` | Everything that must be finished first. `--schedule`, `--period`, `--assignee`, `--unscheduled`, `--unassigned`, `--dry-run`. Aliases: `lpm blockers`, `lpm prerequisites` |
2546
+ | `lpm period <id>` | How it stands. `--on`, `--off`, `--dates`, `--start-now`, `--complete`, `--carry-over`, `--dry-run` |
2547
+ | `lpm instructions [<id>]` | Print the working brief for an issue. `--id`, `--template`, `--no-comments`, `--list`, `--init`, `--force`. Aliases: `lpm brief`, `lpm context` |
2548
+ | `lpm team` | Roster and load. `--period <id>`, `--open`. Alias: `lpm roster` |
2549
+ | `lpm queue simulate` | Work the queue through as one person. `--user <id\|name>`, `--role <id\|name>`, `--unassigned`, `--parked`, `--limit <n>`, `--skipped` |
2550
+ | `lpm queue agent` | Drain the queue with the pi coding agent. `--user`, `--max-tasks`, `--model`, `--effort`, `--commit`, `--unassigned`, `--parked`, `--timeout`, `--command-timeout`, `--plain`, `--file`, `--dry-run` |
2551
+ | `lpm remote [<sub>]` | Mirror the board onto an external tracker. No subcommand lists the remotes. `connect`, `push [<id>…]`, `pull [<key>…]`, `sync`, `status`, `ledger`, and the less common `add`, `login`, `setup`, `rm`, `log`, `resolve`, `link`, `unlink`, `decouple`, `relink`, `rebase`. `--remote <name>`, `--all`, `--children`, `--recursive`, `--parent <id>`, `--scope <id>`, `--force`, `--purge`, `--dry-run`, `--changed`, `--limit N`, `--yes`, `--refresh`, `--since <iso>`, `--json` |
2552
+ | `lpm check` | Validate. `--fix` repairs, `--strict` also fails on warnings |
2553
+ | `lpm ui` | Open the board in a browser. `--port`, `--host`, `--no-open`, `--api-only`, `--experimental`. Alias: `lpm web` |
2554
+ | `lpm export` | Publish the board as a static site. `-o/--out`, `--site <dir>`, `--workflow`, `--pretty`. Alias: `lpm publish` |
2555
+ | `lpm mcp` | Serve the board to AI agents over MCP. `--user`, `--profile`, `--read-only`, `--allow-remote`, `--root` |
2556
+ | `lpm mcp setup` | Write the MCP config a host needs. `--file`, `-o/--output`, `--name`, `--user`, `--profile`, `--read-only`, `--allow-remote`, `--print`, `--force` |
2557
+ | `lpm agent` | Install the agents, skills and MCP config into a project. `--target`, `--type`, `--project`, `--global`, `--dir`, `--name`, `--user`, `--profile`, `--read-only`, `--no-mcp`, `--force`, `--dry-run` |
2558
+ | `lpm hcm init` / `build` / `remove` | Register the agents, skills and MCP server as hcm bundles; render them without registering; unregister them. `--dir`, `--dev` |
2559
+
2560
+ `lpm check` exits 1 when errors remain, so it drops straight into CI or a
2561
+ pre-commit hook. Run `lpm <command> --help` for full options.
2562
+
2563
+ ### Environment
2564
+
2565
+ | Variable | What it does |
2566
+ | --- | --- |
2567
+ | `LPM_BOARD_PATH` | The board to work on, from any folder |
2568
+ | `LPM_USER` | Act as this resource ([who you are](#working-as-a-team-member)) |
2569
+ | `LPM_PROFILE` | The profile file to use ([profiles](#profiles-giving-one-developer-one-part-of-the-board)) |
2570
+ | `LPM_LOCK_TIMEOUT_MS`, `LPM_LOCK_STALE_MS`, `LPM_NO_LOCK` | The write lock ([sharing a checkout](#several-people-and-agents-one-checkout)) |
2571
+
2572
+ Normally `lpm` finds the board by walking up from the current folder, the way
2573
+ `git` finds a repository. `LPM_BOARD_PATH` says which board instead, so the CLI
2574
+ works from anywhere — a scratch directory, your home folder, an agent host that
2575
+ starts somewhere you did not choose:
2576
+
2577
+ ```bash
2578
+ export LPM_BOARD_PATH=~/work/payments/.lpm
2579
+ cd /tmp && lpm task next # still the payments board
2580
+ ```
2581
+
2582
+ Point it at the `.lpm` folder or at the folder holding it; either reads. If it
2583
+ names no board that is an error rather than a fall back to the search, because
2584
+ a typo that quietly worked on whichever board you happened to be standing in is
2585
+ the failure nobody notices.
2586
+
2587
+ Two commands are deliberately deaf to it. `lpm init` always creates the board
2588
+ where you told it to — creating a board somewhere is not the same as reading
2589
+ the one you work on — and says so if the variable points elsewhere. And an
2590
+ explicit path wins: `lpm mcp --root`, `lpm mcp setup --root` and `lpm agent
2591
+ --project` are about the folder they name.
2592
+
2593
+ ## Web UI
2594
+
2595
+ `lpm ui` opens the board in a browser: a left-to-right dependency graph, a
2596
+ resizable drawer with a table, the increments and sprints, a Gantt chart and the
2597
+ team roster, and a side panel for editing whatever is selected.
2598
+
2599
+ ```bash
2600
+ lpm ui # serve the board and open a browser
2601
+ lpm ui --port 8080 # somewhere else
2602
+ lpm ui --api-only # just the JSON API, for the dev server below
2603
+ lpm ui --experimental # also offer features that are not finished yet
2604
+ ```
2605
+
2606
+ The server binds to `127.0.0.1`, serves one board — the checkout it was started
2607
+ in — and has no notion of users or sessions. It is the same engine the CLI uses,
2608
+ behind a small REST API.
2609
+
2610
+ **Tracker remotes are experimental in the web UI.** Mirroring the board onto
2611
+ Jira, GitHub or Linear — the Sync tab's tracker panel and its **Connect…**,
2612
+ readiness and **to fix** windows, the drift badges on the canvas, the **Push**
2613
+ and **Pull** entries in the canvas menu and the side panel's remote and conflict
2614
+ sections — is shown only by `lpm ui --experimental`. Without the flag the server
2615
+ does not register the `/api/remotes` routes at all, and the Sync tab offers
2616
+ [sharing the board through git](docs/git-sync.md) and nothing else. The bullets
2617
+ below marked *(experimental)* describe that mode. The `lpm remote` commands are
2618
+ unaffected.
2619
+
2620
+ ### Views
2621
+
2622
+ Work in the UI happens inside a **view**: a saved slice of the board, stored as
2623
+ JSON in `.lpm/views/<name>.json` and committed like everything else. A view
2624
+ records which issues are on the canvas, where they sit, how big they were
2625
+ dragged, which subflows are collapsed, which levels are drawn as badges rather
2626
+ than as nodes, how the panes are sized, and any edits that have not been written
2627
+ to the board yet.
2628
+
2629
+ Every change is queued in the open view and autosaved there — but there is
2630
+ nothing to press to send it. A debounce (~1.5s, so a dragged slider or a few
2631
+ fields typed in a row still land as one write) replays the queue through the
2632
+ same operations the CLI uses (`createIssue`, `updateNode`, `retypeNode`,
2633
+ `moveNode`, `removeNode`) and reports anything the board rejected — a push is
2634
+ partial rather than all-or-nothing, so one bad edit does not hold the rest
2635
+ up. The top bar has no Push or Pull button any more, just a status label —
2636
+ *pushing…*, or how many edits are still waiting for the debounce to fire —
2637
+ because "have I written this down yet?" stopped being a question anybody has
2638
+ to ask. Pulling is the same: the open view polls the board every few seconds
2639
+ and lays your unpushed work back over whatever it finds, so a file changed by
2640
+ another window, `lpm`, or an agent working the queue shows up here on its
2641
+ own. That poll is not a filesystem watcher — it is a plain re-read on a timer
2642
+ — so "instantly" is closer to "within a few seconds".
2643
+
2644
+ Anything the board refuses — a rejected change, a cycle, a board that will not
2645
+ load — is a red card across the top of the screen that stays until it is
2646
+ dismissed. Anything merely informative is a quieter one that clears itself.
2647
+
2648
+ Because the queue lives in the view file, closing the tab loses nothing, and a
2649
+ teammate who pulls your branch sees the same canvas you were looking at.
2650
+
2651
+ ### What it does
2652
+
2653
+ - **Home** — before you open a view, three readings of the board. *Now* is the
2654
+ increment and sprint running today, what is in flight in it, and what is
2655
+ unblocked and unstarted, ranked the way `lpm task next` ranks work; when
2656
+ today's sprint is finished it looks ahead to the next one with work in it and
2657
+ says so. *Just added* is the last handful of issues written, newest first.
2658
+ *Open pathways* ignores the calendar and asks the graph instead: the epics and
2659
+ features whose blockers are all done, so work could start anywhere inside
2660
+ them, ordered by how much finishing one would release.
2661
+ - **Graph** — nodes carry a target handle on the left and a source handle on the
2662
+ right; issues with children render as collapsible subflows, and collapsing one
2663
+ reroutes its descendants' dependencies onto it. Colour follows status, the
2664
+ icon follows type, and edges into work in progress animate. Edges are routed
2665
+ orthogonally, and selecting a node lights up everything one hop from it —
2666
+ its blockers, what it blocks, and the edges between — while selecting an edge
2667
+ lights up the two issues it joins. Those edges are drawn in a contrasting
2668
+ colour with the dash travelling the way the dependency runs, so the chain you
2669
+ asked about is findable in a graph of hundreds; everything else fades back.
2670
+ Edges *leaving a flagged issue* are drawn red and slightly heavier with
2671
+ nothing selected at all, so the work stalled behind a flag is a shape you can
2672
+ see from across the board rather than something to go clicking for — a folded
2673
+ feature holding a flagged story shows it too.
2674
+ A scheduled issue carries a badge naming the periods it sits in
2675
+ (`PI-1 · Sprint A`), and the ones in the period running *today* are washed in
2676
+ amber with a strip along the top — so the work to pick up now is a shape on
2677
+ the canvas rather than something to go looking for. The layout flows left to
2678
+ right along the dependencies, subflows stretch in both directions to hold what
2679
+ is inside them, and issues added later are set down clear of the ones already
2680
+ placed. *Arrange* lays the whole thing out again.
2681
+ - **Interaction** — context menus on the canvas, on a node and on an edge; drag a
2682
+ node onto another to reparent it, hold <kbd>Alt</kbd> and drop one onto an
2683
+ edge to splice it in, rubber-band or <kbd>Shift</kbd>-click to multi-select,
2684
+ <kbd>Ctrl</kbd>+<kbd>C</kbd>/<kbd>V</kbd> to copy structures. Holding
2685
+ <kbd>Ctrl</kbd> turns every surface into the pane: dragging pans the camera
2686
+ even when it starts inside a node, which is what makes a subflow the size of
2687
+ the screen navigable. A pan does not stop at the edge of the display — the
2688
+ cursor is taken off screen for the length of the drag and given back where it
2689
+ started, so a board several screens wide crosses in one gesture. Select a node
2690
+ to get resize handles: the size is saved with the view, and a subflow will not
2691
+ be dragged smaller than its contents.
2692
+ - **Editing a selection** — <kbd>Shift</kbd>-click or rubber-band as many issues
2693
+ as you like, then right-click *anywhere* — one of them, or the empty canvas —
2694
+ and the menu addresses all of them: **Change status**, **Assign to** (everyone
2695
+ on the roster, the generic pools listed after the people), **Schedule into**
2696
+ (the period tree, indented, plus the backlog), remove from the view, delete.
2697
+ A tick means every selected issue is already there. The same menu is on a
2698
+ table row, and dropping a teammate from the roster onto one of several
2699
+ selected nodes assigns all of them — so fifteen stories reach somebody in one
2700
+ gesture rather than fifteen trips through the side panel. Right-clicking never
2701
+ changes the selection unless the click lands outside it.
2702
+ - **Push and pull** *(experimental)* — right-click a selection for **Push** and **Pull** when
2703
+ the board mirrors a remote. Push files exactly what is selected, never the
2704
+ subtrees beneath it; Pull fetches the twins of the selected documents, and
2705
+ counts them, so pulling something with no twin is offered as nothing rather
2706
+ than as a smaller pull. The side panel does the same for the one document it
2707
+ is showing, with the twin's key linked out to the tracker, a line saying
2708
+ which way it has drifted, and an **include everything under it** tick for a
2709
+ container. Both go through the same run as the Sync tab, so the refusal to
2710
+ sync over queued edits holds here too: Push your edits to the board first —
2711
+ the board is what travels upstream. Until the remote's drift report arrives
2712
+ — it reads every twin, which on a large tracker takes a minute or two — the
2713
+ panel says *Reading the remote…* and Pull is greyed with the same reason,
2714
+ rather than showing nothing and looking as if the document were not
2715
+ mirrored.
2716
+ - **Before a push writes** *(experimental)* — every push asks the tracker two questions first
2717
+ (who it can assign work to, which periods it holds) and shows what will not
2718
+ land the way the board says: a person with no account upstream, an account
2719
+ that went stale, a pool, a sprint nobody filed. Each row offers the same two
2720
+ answers — **leave it** (filed blank, and a later push writes it once the far
2721
+ side exists) or **fix it** (the account written onto that person's roster
2722
+ document, the period filed in the same run, an assignee the board has lost
2723
+ cleared) — and the dialog offers the third, **cancel**. Only a board
2724
+ contradicting itself blocks the push; everything else is a degradation you
2725
+ can accept with one click.
2726
+ - **How the mirror is doing** *(experimental)* — the Sync tab shows the local half of the drift
2727
+ report as soon as the board opens: which documents have twins, what has never
2728
+ been pushed, what has been edited here since the last sync. Asking the tracker
2729
+ what moved *upstream* needs a credential and can fail, so it reads the project
2730
+ in one paginated listing, and while it runs the tab says how many twins it is
2731
+ comparing and for how long, can stop it, and keeps any failure on screen. The
2732
+ result of that read is remembered in the browser, so reopening the board shows
2733
+ the same "checked" state and counts it showed last time — with a "checked
2734
+ Xm/h/d ago" note beside them — rather than reading the tracker again before
2735
+ anybody asked; **Check again** forces a fresh read, and any sync forgets the
2736
+ cache for the remote it just wrote to, since that run is exactly what would
2737
+ make the old numbers wrong. Because it reads the whole project it also reports
2738
+ **incoming** work — issues in the tracker with no document here yet, which is
2739
+ what a story somebody added upstream looks like. Each row names the local
2740
+ document a pull would file it under, with **Pull** beside it. At the terminal
2741
+ the same split is `lpm remote status --local`, and `--changed` is the cheaper
2742
+ incremental read (the terminal has no browser cache to fall back on, so it
2743
+ always reads live when asked).
2744
+ - **Which documents, and why** *(experimental)* — under the counts is a table of what a sync
2745
+ would actually do: one row per document, the direction it moves, and the
2746
+ fields that say so. Clicking a count (**to push**, **to pull**,
2747
+ **conflicts**) scrolls to the table and narrows it to that count; clicking it
2748
+ again shows everything. Each row links to the document on the canvas. Edits this remote
2749
+ *cannot* store are deliberately not in the table — a sync will not write
2750
+ them, so they would only bury what it will. The **to fix** chip counts them
2751
+ instead, and opens a window grouping them by the cause they share (one person
2752
+ with no account value, say) with the change that clears each one — a window
2753
+ rather than a panel section, because most of these are differences between
2754
+ the two tools rather than mistakes, and a list of things nobody can fix this
2755
+ week should not be on screen every time the tab is opened.
2756
+ - **Coverage** *(experimental)* — the Sync tab lists what the remote is *missing* around what it
2757
+ already holds: the work inside a filed container, the containers above filed
2758
+ work, the period mirrored issues were filed without, and the far end of a
2759
+ dependency that could not be written. Each group says what it costs and each
2760
+ row names the mirrored documents behind it, so nothing in the list is
2761
+ unexplained. Tick what you want and **Push selected** files exactly those —
2762
+ a container ticked beside its contents is created first and they are filed
2763
+ under it in the same run. It is read from the board and the link store rather
2764
+ than from the tracker, so it is on screen at once and re-read after every
2765
+ push; the side panel shows the same for one document and the canvas menu
2766
+ offers it for a selection. A document you decoupled, or one outside the
2767
+ remote's scope, is reported rather than offered — both were decided.
2768
+ - **Connect a remote** *(experimental)* — the Sync tab's **Connect…** (or **Connect a remote…**
2769
+ on a board with none) asks which tracker, where its project is, and the
2770
+ credential, in one form drawn entirely from what each provider declares: the
2771
+ fields it needs and which are optional, a worked example beside each, the
2772
+ page where its token is created, and which credential keys are secret. A
2773
+ secret field is masked; a value that is no secret to look at, such as an
2774
+ account email, is not. The credential is stored in `.lpm/credentials.json`,
2775
+ which is git-ignored, and is never shown again — the page reports only where
2776
+ each value comes from. **Connection** opens the selected remote: edit where
2777
+ it points, replace an expired token (a blank keeps what is stored), **Test
2778
+ connection** to ask the tracker about itself — what it answers
2779
+ unambiguously, such as a Jira board id, is written for you, and what it
2780
+ cannot decide is offered as a choice — and remove it. A remote that already
2781
+ mirrors documents cannot be pointed at a different project in place: remove
2782
+ it and connect again.
2783
+ - **Upstream work** — right-click one issue for **Add upstream dependencies**
2784
+ and everything that has to be finished before it lands on the canvas: the
2785
+ chain of edges behind it, the open stories inside each blocking container, and
2786
+ the features and epics around them so nothing floats free. Nothing is edited —
2787
+ it is a way of looking, and the canvas is where "absolutely everything
2788
+ required to close this" is a shape rather than a list. **Schedule upstream
2789
+ dependencies** beside it is the other half: every unclaimed piece of that work
2790
+ is queued into the same sprint, for the same person or pool, as the issue
2791
+ waiting on it. Work somebody already holds is left alone and reported, so a
2792
+ chain six people are already on does not come back as "nothing to schedule".
2793
+ Both are `lpm upstream` [described above](#what-has-to-happen-first-upstream-work),
2794
+ reading the same rule.
2795
+ - **Options ▸ DAG ▸ Hierarchy display** — how deep the canvas draws. A board four
2796
+ levels deep drawn as boxes inside boxes is a picture of the hierarchy, not of
2797
+ the work: the dependencies run between the stories at the bottom, and every
2798
+ level above is a frame around the part you are reading. Set a level to
2799
+ **badges** and its issues leave the canvas — everything under them moves up a
2800
+ level wearing their id, so the graph is the work and the structure is a label.
2801
+ Several levels can be badged at once, a level with nothing underneath to carry
2802
+ its badge stays a node, and the choice is saved with the view (and published
2803
+ with it). The canvas is laid out again when it changes, because badging a
2804
+ level moves everything below it.
2805
+ - **Options ▸ Planning** — whether this view plans with a calendar at all.
2806
+ *Sprints and increments* is the default and gives the drawer its Periods and
2807
+ Gantt tabs. *Queue* is for the way plenty of teams actually work — nobody
2808
+ plans a fortnight, work is taken off the top as the graph unblocks it — and
2809
+ swaps both tabs for the Queue. Nothing about the board changes either way:
2810
+ the same documents, the same dependencies, a different question in front of
2811
+ you. A board whose config declares no period types is always in the second
2812
+ mode, because there is nothing to plan with.
2813
+ - **Dropping something where it does not fit** — a story dragged onto a program
2814
+ skips two levels, and there are exactly two honest answers, so the app asks
2815
+ rather than guessing: *change its type*, and the story becomes an epic, or
2816
+ *create containers to hold it*, and the epic and feature it was missing are
2817
+ built for it (titles editable before they exist) while it stays a story with
2818
+ everything underneath it untouched. That is `lpm convert --under` and
2819
+ `--build-parents`, from the same planner.
2820
+ - **Table** — the whole board as collapsible rows, with inline editing and a
2821
+ checkbox per row for what appears on the canvas. The checkbox in the header
2822
+ puts everything the filter matches on the canvas at once, and
2823
+ <kbd>Shift</kbd>- or right-clicking a row's checkbox takes that issue's whole
2824
+ subtree. Quick filters answer the usual questions without typing — what is in
2825
+ progress, what just finished, what was just added, what is on the canvas — and
2826
+ *Show down to* folds the tree to a level, so one click leaves the programmes
2827
+ showing with their epics closed. Clicking a row moves the graph to it, and
2828
+ selecting anything anywhere scrolls this list to it and opens whatever it was
2829
+ folded inside.
2830
+ - **Periods** — the timeline as boxes of work, nested the way the periods are:
2831
+ each sprint is drawn *inside* the increment it belongs to, one band under
2832
+ another down the pane, with everything unscheduled in a box at the end. Every
2833
+ level holds cards of its own, because an epic can sit in the increment while
2834
+ its stories sit in the sprints. Drag a card into a box to schedule it — a drop
2835
+ lands in the innermost box under the pointer — or select issues anywhere and
2836
+ press *← selection* on the box that should have them; the × on a card takes it
2837
+ back out.
2838
+
2839
+ Work can also come straight off the canvas: **hold Alt and drag a node into a
2840
+ box**. What lands in the sprint is the *work under* what was dragged — drop an
2841
+ epic and its user stories are scheduled, drop a feature and only its own are,
2842
+ drop a story and it goes in by itself. Containers are never scheduled by this,
2843
+ because a feature is not a thing you pick up, it is the name of the stories
2844
+ you do; nor are the sub-tasks inside an `atomic` story, which travels whole. Dropping onto the unscheduled box takes the whole subtree back out
2845
+ again.
2846
+
2847
+ Nothing on the canvas moves while you do it: the node stays exactly where it
2848
+ is, the camera holds still, and no position is saved. Being scheduled into a
2849
+ sprint is not a change to the picture, so the picture does not change — only
2850
+ the box under the pointer lights up. Escape puts it down with nothing altered.
2851
+ Without Alt a drag still moves the node, as it always did.
2852
+
2853
+ Headers edit the period's name and dates in place, and each box
2854
+ counts its issues and its effort: its own, and the total including everything
2855
+ nested inside it. Every box folds to a single line — when it runs, what is in
2856
+ it, how many boxes are inside — with *Collapse all* and *Expand all* in the
2857
+ toolbar; a folded box still takes a drop, so a run of folded sprints is a fast
2858
+ way to file a card into a distant one.
2859
+
2860
+ Drag a box by its grip to move it in the running order. A sprint has no
2861
+ position to change — the order sprints run in *is* their dates — so dropping
2862
+ one on another rewrites the run: each period keeps its own length, the gaps
2863
+ between them stay where the calendar had them, and the whole sequence shifts
2864
+ around the move.
2865
+
2866
+ The period that is running is washed in the same amber the canvas uses, and
2867
+ the increment holding it is outlined, so "where are we?" is answered by
2868
+ looking. Every other period offers **Start now**, which is the button for the
2869
+ Monday when the plan and the team disagree: it moves that sprint onto today
2870
+ keeping how long it runs, takes the periods nested inside it along by the same
2871
+ number of days, closes whatever *was* running the day before, and stretches
2872
+ the increment around the new dates. It always says exactly which dates it is
2873
+ about to rewrite and waits for an answer — starting a sprint is a sentence
2874
+ somebody says in a stand-up, and moving six documents' dates is not.
2875
+
2876
+ Beside that sits the **switch**, which runs in parallel with the calendar and
2877
+ is the control for a team that does not plan by date, or one that has to
2878
+ reroute people mid-sprint. On is on whatever the dates say; off parks the
2879
+ period and everything nested inside it, and its work sinks to the bottom of
2880
+ every queue; clicking again hands it back to the dates. Switching a period on
2881
+ when today has fallen outside it asks whether to move the dates to match —
2882
+ reusing the plan in an increment that was started and abandoned is exactly a
2883
+ restart — and declining still flips the switch, leaving the dates as the
2884
+ record of what was planned.
2885
+
2886
+ A period whose end date has passed with work still open in it is drawn in
2887
+ **red** with a **Fix…** button, which offers the two honest answers side by
2888
+ side: mark the open work done, which records that the team stopped, or carry
2889
+ it into the next period and leave what was finished where it was delivered.
2890
+ No period is invented to hold it, so carrying work down a run makes the last
2891
+ sprint's backlog grow — and when there is no next period the dialog says so
2892
+ instead of offering the button. Both are queued like any other edit, so they
2893
+ can be read in the pending list before a push.
2894
+
2895
+ Adding a period seeds its dates from the one before it,
2896
+ so filling a quarter is a row of clicks; the × deletes one with everything
2897
+ nested in it, and *Clear all* deletes the whole timeline after a confirmation.
2898
+ Deleting periods never deletes work: the issues in them simply become
2899
+ unscheduled.
2900
+ - **Queue** — the same board for a team that does not plan in sprints. *Ready*
2901
+ is every unstarted work unit with no unfinished blocker, in the order `lpm task
2902
+ next` would offer them: priority first, then how much finishing one would
2903
+ release. *In progress* is what is being worked on, *Blocked* says what each
2904
+ waiting issue is waiting on rather than hiding it, and *Just finished* is the
2905
+ tail. Drag a card between lanes, or press *Start* and *Finish* — either way it
2906
+ is one status change. This is what the drawer offers instead of Periods and
2907
+ Gantt when a view is set to work off the queue (Options ▸ Planning), and the
2908
+ only thing it offers on a board whose config declares no periods at all.
2909
+ - **Gantt** — the plan on a date scale, read at whichever level you want:
2910
+ *by period*, with the issues scheduled in each one nested underneath it, or
2911
+ *by hierarchy*, where every parent gets a bar covering the work beneath it
2912
+ (dashed when the dates are borrowed from that work rather than its own
2913
+ period). Rows fold, and *Show down to* folds them a whole level at a time —
2914
+ one click for increments, sprints, or any level of the issue hierarchy inside
2915
+ them. The critical path is highlighted.
2916
+ - **Team** — the roster with load against capacity, grouped by discipline, team,
2917
+ assignment or pool. Drag a card onto a node to assign it. The pencil on a card
2918
+ opens the rest of a resource: its type, its team, its attributes and notes,
2919
+ and the pools it covers — from either side, so a pool's own editor is a list
2920
+ of who can pick work up from it.
2921
+ - **Side panel** — every reserved field and every configured attribute with a
2922
+ type-appropriate editor, the body rendered as markdown (*Edit* — or a
2923
+ double-click — swaps it for the source), the immediate upstream and downstream
2924
+ issues (plus, on a container, the ones the work inside it puts it in order
2925
+ with — see [dependencies](#dependencies); those carry no unlink button,
2926
+ because nothing is written on the document to break), and *Break down*, which
2927
+ splits an issue into N pieces either as
2928
+ children or as a chain that replaces it — rewiring both ends of the graph
2929
+ either way. Drag its edge to widen it; the width is saved with the view.
2930
+ - **Panes that spend the room** — dragging the drawer taller or the panel wider
2931
+ grows the type and spacing inside it, so a pane made bigger shows bigger rows
2932
+ rather than more empty space. Growth is gentler than the drag and stops at
2933
+ half again, because the point of the drag was to see more.
2934
+
2935
+ ### Building it
2936
+
2937
+ The app lives in [`web/`](web/) and is a separate package, so the published CLI
2938
+ keeps its four runtime dependencies.
2939
+
2940
+ ```bash
2941
+ make build # engine + app + viewer
2942
+ make ui # serve a throwaway demo board in a browser
2943
+ make site # export the demo board as a static site and serve that
2944
+ make dev # rebuild both as you edit; reload the browser to see changes
2945
+ make dev-web # Vite dev server with hot reload, against the demo board
2946
+ ```
2947
+
2948
+ `make ui` and `make dev-web` create a sample board in `.demo/` first, so you have
2949
+ something to look at without touching a real one. `lpm ui` serves `web/dist`, and
2950
+ says so if it has not been built.
2951
+
2952
+ ## Publishing a board
2953
+
2954
+ `lpm ui` needs a checkout, a Node install and a running process. `lpm export`
2955
+ needs none of that: it writes the board into a single JSON file and, with
2956
+ `--site`, drops a read-only viewer next to it. The result is an ordinary static
2957
+ site — point GitHub Pages at it and anyone with the link can read the graph.
2958
+
2959
+ ```bash
2960
+ lpm export # refresh .lpm/board.json
2961
+ lpm export --site docs # a complete site in docs/, ready for Pages
2962
+ lpm export --site docs --workflow # ...and a workflow that keeps it current
2963
+ ```
2964
+
2965
+ `--site docs` writes `docs/index.html`, its assets, `docs/board.json` and a
2966
+ `.nojekyll`. Commit the directory, then **Settings → Pages → Source: `/docs`**,
2967
+ and the board is at `owner.github.io/repo/`. `--workflow` instead writes
2968
+ `.github/workflows/lpm-board.yml`, which regenerates `board.json` from `.lpm/`
2969
+ and deploys on every push — for that one, set **Source: GitHub Actions**.
2970
+
2971
+ The viewer is deliberately less than the editor: the dependency graph, a picker
2972
+ for the board's saved views, and a details panel for whatever is selected. No
2973
+ editing, no drawer, no comments, no push — there is no server to push to. You
2974
+ can still pan, zoom, fold subflows, drag nodes around and re-*Arrange*; tidying
2975
+ the picture you are reading is not editing the board.
2976
+
2977
+ Boards with no views in `.lpm/views/` still work: the picker always offers
2978
+ **All issues**, the whole board with every parent folded, so it opens at its top
2979
+ level and unfolds where you are interested.
2980
+
2981
+ ### Reading someone else's board
2982
+
2983
+ The published page is a viewer, not just a rendering of one board. Give it a
2984
+ repository and it will read that one instead:
2985
+
2986
+ ```
2987
+ https://acme.github.io/plan/?repo=other-org/their-repo
2988
+ https://acme.github.io/plan/?repo=other-org/their-repo@release/2026
2989
+ https://acme.github.io/plan/?repo=other-org/their-repo&path=docs/board.json
2990
+ https://acme.github.io/plan/?src=https://example.com/anywhere/board.json
2991
+ ```
2992
+
2993
+ `?repo=` reads `.lpm/board.json` over `raw.githubusercontent.com`, so the other
2994
+ repository needs nothing installed — just a committed export. It is one GET of
2995
+ one file: no GitHub API, no token, no rate limit worth planning around, and
2996
+ public repositories only. `#/view/<id>` in the address names the open view, so
2997
+ any picture on screen is a link.
2998
+
2999
+ ### What travels, and what does not
3000
+
3001
+ The exported file is the board **as committed**. A view's queued-but-unpushed
3002
+ changes are somebody's draft and never appear in it, and a view is narrowed to
3003
+ its `members` and `layout` — drawer and panel state mean nothing to a reader.
3004
+ Members naming a document that no longer exists are dropped.
3005
+
3006
+ Everything else in the file is what `.lpm/` already publishes to anyone who can
3007
+ read the repository, including issue bodies. Treat `board.json` as exactly as
3008
+ public as the board it came from. The viewer renders bodies as plain text, never
3009
+ as markup, because it will happily read a board from a URL you were handed.
3010
+
3011
+ `board.json` is generated, so it goes stale like any build output. Either let
3012
+ the workflow rebuild it, or re-run `lpm export` before you commit.
3013
+
3014
+ ## Using the engine directly
3015
+
3016
+ The CLI is a thin shell over `src/core`, which is exported as a library so a GUI
3017
+ can drive the same board:
3018
+
3019
+ ```ts
3020
+ import {
3021
+ findBoardPaths, loadBoard, createIssue, createPeriod, createResource,
3022
+ linkIssue, issuesInPeriod, periodChain, checkBoard,
3023
+ nextTasks, currentTasks, resourceLoad, currentUser, currentScope,
3024
+ } from 'light-plan';
3025
+
3026
+ const paths = findBoardPaths()!;
3027
+ const board = loadBoard(paths);
3028
+
3029
+ board.roots; // issue tree, each node with .children
3030
+ board.periodRoots; // period tree
3031
+ board.resourceRoots; // roster tree
3032
+ board.byId.get('LP-4'); // flat lookup
3033
+ board.dependents.get('LP-4'); // derived inverse of depends_on
3034
+ board.coveredBy.get('RS-4'); // derived inverse of covers
3035
+ issuesInformedBy(board, 'LP-4'); // what rests on that research, resolved
3036
+ issuesInPeriod(board, 'TL-1'); // sprint/increment contents
3037
+ periodChain(board, 'TL-2'); // [increment, sprint]
3038
+ nextTasks(board, 'RS-1'); // ranked recommendations for one person
3039
+ nextTasks(board, 'RS-1', { scope: currentScope(board) }); // ...through their profile
3040
+ currentTasks(board, 'RS-1'); // what they have in flight
3041
+ resourceLoad(board, { periodId: 'TL-2' }); // who is carrying what
3042
+ checkBoard(board); // Problem[]
3043
+ ```
3044
+
3045
+ Everything is synchronous filesystem work with no ambient state, so it is easy
3046
+ to test and easy to call from an editor extension or a desktop app.
3047
+
3048
+ ### How `src/core` is organised
3049
+
3050
+ Eight layers, each depending only on the ones above it:
3051
+
3052
+ | Folder | Responsibility |
3053
+ | --- | --- |
3054
+ | `model/` | What an issue, period, resource, type and problem *are*, plus pure logic over them (attribute types, the dependency graph). No I/O — the one layer everything else may depend on. |
3055
+ | `config/` | Parsing `.lpm/config.yml` into a validated `BoardConfig` (`schema.ts`) and answering questions about it (`lookup.ts`). Every rule about hierarchy depth, statuses and type namespaces resolves here. |
3056
+ | `storage/` | How a board is encoded on disk: `paths`, `frontmatter`, `document` (serialize/write), `comments` (the work log beside a document), `state` (id counters), `local` (current user and profile path), `views` (saved UI views), `templates` (the context layouts), `git` (authorship). Knows the layout, not what makes it valid. |
3057
+ | `board/` | Reading all three collections into a `LoadedBoard` (`load.ts`), navigating it (`query.ts`), narrowing it to one person's part of it (`scope.ts`) and recommending work (`tasks.ts`). |
3058
+ | `profile/` | The file one developer is handed: parsing it (`schema.ts`) and finding the one in force (`current.ts`). Never board truth — `check` neither reads one nor knows it exists. |
3059
+ | `instructions/` | One issue plus its ancestry, rendered as a working brief: the little template language (`template.ts`), the values it can see (`context.ts`), the layout every board falls back to (`builtin.ts`) and which layout to use (`instructions.ts`). Read-only, like `tasks.ts`. |
3060
+ | `operations/` | The commands that change a board: `init`, `create`, `update`, `retype`, `move`, `link`, `comment`, `remove`, `user`, `profile`. Each validates fully before touching the filesystem. |
3061
+ | `validation/` | `check.ts` (read-only, reports everything) and `fix.ts` (repairs exactly what check marks `fixable`). Sharing a folder is what keeps the two from drifting. |
3062
+
3063
+ `errors.ts` sits outside the stack — any layer may throw a `BoardError`.
3064
+
3065
+ Each folder has an `index.ts` describing it, and `src/core/index.ts` re-exports
3066
+ them all, so importing from `light-plan` is unaffected by the internal layout.
3067
+
3068
+ Three more folders sit above the engine, shared by everything that drives it:
3069
+
3070
+ | Folder | Responsibility |
3071
+ | --- | --- |
3072
+ | `src/shared/` | The contract: the DTOs that cross the wire, the `Change` protocol edits are expressed in, and `plans.ts` — the multi-step edits (split, insert, copy, convert, reparent) as pure functions from a board to a list of changes. Imports nothing, so it compiles for Node and the browser alike. |
3073
+ | `src/sync/` | Replaying a change list through the engine, and mapping the core model onto the DTOs. |
3074
+ | `src/server/`, `src/mcp/` | The two remote front ends: HTTP for the web app, MCP for agents. |
3075
+
3076
+ That is why `lpm split`, the canvas's *Break down* button and an agent's
3077
+ `split_issue` tool produce the same board: all three plan with `src/shared` and
3078
+ apply with `src/sync`.
3079
+
3080
+ ## Development
3081
+
3082
+ There are two packages here: the engine and CLI at the root, and the web app
3083
+ under `web/` with its own `node_modules`. The [`Makefile`](Makefile) drives both,
3084
+ so you rarely have to think about which is which. `make` on its own lists
3085
+ everything.
3086
+
3087
+ | Target | What it does |
3088
+ | --- | --- |
3089
+ | `make doctor` | Check Node, npm, git and the installed dependencies before you start |
3090
+ | `make setup` | Fresh clone to working `lpm`: install both packages, build, `npm link` |
3091
+ | `make dev` | Watch `src/` and `web/src/` and rebuild on every change |
3092
+ | `make dev-web` | Vite dev server with hot reload, against the demo board |
3093
+ | `make ui` | Serve a throwaway demo board in a browser |
3094
+ | `make mcp` | Serve that demo board to an agent over MCP |
3095
+ | `make demo` | Create that demo board in `.demo/`, seeded with issues, sprints and a roster |
3096
+ | `make test` | Both test suites |
3097
+ | `make typecheck` | `tsc` over the engine, `svelte-check` over the app |
3098
+ | `make verify` | Typecheck, test and build: what a pull request should pass |
3099
+ | `make dist` | Build a publishable tarball in `release/` and check its contents |
3100
+ | `make publish CONFIRM=yes` | Verify, build the tarball and publish it to npm |
3101
+ | `make version-patch` | Bump the version, commit and tag it (also `-minor`, `-major`) |
3102
+ | `make outdated` | Dependencies with newer releases |
3103
+ | `make clean` / `make fresh` | Remove build output / wipe everything and set up again |
3104
+
3105
+ `make dev` is the working loop: it links the CLI globally and keeps rebuilding,
3106
+ so `lpm` and `lpm ui` always run the code you just edited — reload the browser to
3107
+ pick up the web app. For UI work, `make dev-web` is faster: Vite serves the app
3108
+ with hot module reload and proxies the API to a real board server.
3109
+
3110
+ Targets that produce files depend on their sources, so `make build` does nothing
3111
+ when nothing has changed. Every target runs from `cmd.exe` and PowerShell as well
3112
+ as from a POSIX shell — GNU Make and Node are all it needs (on Windows,
3113
+ `choco install make`).
3114
+
3115
+ Without Make, the same steps are npm scripts:
3116
+
3117
+ ```bash
3118
+ npm run build # compile src/ to dist/
3119
+ npm run typecheck # src + tests
3120
+ npm test # builds, then runs vitest (unit + end-to-end CLI + API)
3121
+
3122
+ npm run install:web # install the web app's dependencies
3123
+ npm run build:all # engine + web app
3124
+ npm run typecheck:web # svelte-check over web/
3125
+ npm run test:web # the web app's unit tests
3126
+ ```
3127
+
3128
+ Runtime dependencies are `yaml` and `zod` (config and schemas), plus `eta` and
3129
+ `acorn` — the template engine behind `lpm instructions` and the parser its safety
3130
+ check reads templates with. Argument parsing uses Node's built-in
3131
+ `util.parseArgs`. The web app is its own package under `web/` with its own
3132
+ dependencies (Svelte 5, SvelteFlow, dagre), and is built to static assets that
3133
+ the `lpm ui` server hands out.
3134
+
3135
+ ### Releasing
3136
+
3137
+ `make dist` builds both packages, packs a tarball into `release/`, and fails if
3138
+ the CLI, the built web app or the templates are missing from it — a package
3139
+ without `web/dist` installs cleanly and then serves an empty page, which is the
3140
+ mistake worth catching before publishing. Install the result anywhere with
3141
+ `npm install -g ./release/light-plan-<version>.tgz` (the `./` matters — without
3142
+ it npm reads the path as a GitHub repository).
3143
+
3144
+ Publishing is `make publish CONFIRM=yes`; the flag is required so it cannot
3145
+ happen by a mistyped target, and it is checked before anything runs. It then
3146
+ runs `make verify` (typecheck, build, assets, both test suites) and `make dist`,
3147
+ and publishes only if both pass. A bare `npm publish` from the repository root is
3148
+ refused by `prepublishOnly`, because it would skip the build and the contents
3149
+ check. A release, start to finish:
3150
+
3151
+ ```bash
3152
+ npm login # once per machine
3153
+ make version-patch # or -minor / -major: bumps, commits, tags
3154
+ make publish CONFIRM=yes # verifies, builds, publishes the tarball
3155
+ git push --follow-tags
3156
+ ```
3157
+
3158
+ ## Scope
3159
+
3160
+ Deliberately not included: a terminal board renderer, burndown charts, time
3161
+ tracking, and any attempt to schedule work for you — `lpm team` reports load, it
3162
+ does not level it, and neither does the web team view or the `team_load` tool.
3163
+ The web app does not do multi-board sessions, authentication, real-time
3164
+ collaboration, or conflict resolution beyond "pull again" — and profiles do not
3165
+ change that: a profile routes work on the CLI and over MCP, `lpm ui` shows the
3166
+ whole board, and nothing anywhere treats a scope as a permission. Flags follow
3167
+ the same line: "the plan owner clears it" is a convention the docs and the agents
3168
+ state, not a rule the engine enforces, because enforcing it would mean inventing
3169
+ the permission model the rest of the tool deliberately does without. The template
3170
+ registry is on the same side of that line: `{{name}}` is replaced with a value
3171
+ and nothing else, because a registry with conditionals and loops in it is a
3172
+ programming language somebody has to learn before they can read the plan. The published viewer
3173
+ is read-only by construction and stays that way: an exported board is a file, and
3174
+ a page that could write to it would need the server this whole feature exists to
3175
+ avoid. `lpm remote` mirrors a tracker; it does not replace one, and it does not
3176
+ grow a server: there are no webhooks and no daemon watching the remote, no
3177
+ federation of two boards, and no attempt to own the remote's own workflow — the
3178
+ board stays the source of truth, and where the remote's rules are stricter they
3179
+ win and are reported rather than forced. `loadBoard()` already
3180
+ returns all three trees, the dependency and coverage indexes, and period
3181
+ membership, so anything missing is a small addition on top of the existing
3182
+ engine rather than a change to it.