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