light-plan 0.0.0-stage → 0.1.1

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