@basementuniverse/kanbn 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +1 -1
- package/skills/kanbn-plan/SKILL.md +211 -0
- package/skills/kanbn-plan/references/index-structure.md +89 -0
- package/skills/kanbn-plan/references/planning-rules.md +58 -0
- package/skills/kanbn-plan/references/task-structure.md +143 -0
- package/skills/kanbn-plan/scripts/check-dependency-cycles.mjs +273 -0
- package/skills/kanbn-plan/scripts/validate-kanbn.mjs +135 -0
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Where <command> is one of:
|
|
|
34
34
|
sort .......... Sort a column in the index
|
|
35
35
|
sprint ........ Start a new sprint
|
|
36
36
|
burndown ...... View a burndown chart
|
|
37
|
+
gantt ......... Generate and view a Gantt chart
|
|
37
38
|
validate ...... Validate index and task files
|
|
38
39
|
archive ....... Archive a task
|
|
39
40
|
restore ....... Restore a task from the archive
|
package/package.json
CHANGED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kanbn-plan
|
|
3
|
+
description: Turn a project brief into Kanbn planning artifacts only. Use this skill to generate or revise a Kanbn board index and task markdown files, including task descriptions, sub-tasks, and dependency relationships, without implementing the project itself.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Kanbn Plan
|
|
7
|
+
|
|
8
|
+
Use this skill when the user wants project planning output in Kanbn format: a board, task files, dependencies, sequencing, scope breakdown, and task descriptions.
|
|
9
|
+
|
|
10
|
+
Do not use this skill to implement the project, edit source code, scaffold files outside `.kanbn/`, or estimate progress from work that has not happened yet.
|
|
11
|
+
|
|
12
|
+
## Core Contract
|
|
13
|
+
|
|
14
|
+
Your output must stay inside the Kanbn planning surface:
|
|
15
|
+
|
|
16
|
+
- `.kanbn/index.md`
|
|
17
|
+
- `.kanbn/tasks/*.md`
|
|
18
|
+
|
|
19
|
+
Do not create application code, tests, CI files, or documentation unrelated to the Kanbn board.
|
|
20
|
+
|
|
21
|
+
If the user asks for planning plus implementation, complete the planning portion in Kanbn format first and keep the planning files cleanly separated from any later work.
|
|
22
|
+
|
|
23
|
+
## Planning Goals
|
|
24
|
+
|
|
25
|
+
Produce a board that is both valid and useful:
|
|
26
|
+
|
|
27
|
+
- Break vague requirements into concrete, reviewable tasks.
|
|
28
|
+
- Capture prerequisite relationships with Kanbn relations.
|
|
29
|
+
- Preserve important scope, assumptions, risks, and milestones in task descriptions.
|
|
30
|
+
- Avoid invented execution history such as fake progress, comments, timestamps, or completed work.
|
|
31
|
+
- Keep the plan maintainable so a human can reprioritise it later.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
### 1. Extract scope before writing files
|
|
36
|
+
|
|
37
|
+
From the user prompt or specification, identify:
|
|
38
|
+
|
|
39
|
+
- project goal
|
|
40
|
+
- major workstreams or epics
|
|
41
|
+
- prerequisites and external dependencies
|
|
42
|
+
- constraints, risks, and open questions
|
|
43
|
+
- any explicit milestones, deadlines, or sequencing requirements
|
|
44
|
+
|
|
45
|
+
If the prompt is underspecified, make minimal planning assumptions and state them in the index description or in task descriptions where relevant.
|
|
46
|
+
|
|
47
|
+
### 2. Choose a board shape
|
|
48
|
+
|
|
49
|
+
If the user does not specify columns, default to Kanbn's standard workflow:
|
|
50
|
+
|
|
51
|
+
- `Backlog`
|
|
52
|
+
- `Todo`
|
|
53
|
+
- `In Progress`
|
|
54
|
+
- `Done`
|
|
55
|
+
|
|
56
|
+
And default options:
|
|
57
|
+
|
|
58
|
+
- `startedColumns: ["In Progress"]`
|
|
59
|
+
- `completedColumns: ["Done"]`
|
|
60
|
+
|
|
61
|
+
Use extra columns only when the project brief clearly benefits from them, for example `Review`, `Testing`, `Release`, or domain-specific approval states.
|
|
62
|
+
|
|
63
|
+
Do not use columns to model architecture layers, epics, or teams when tags or task descriptions would be more appropriate. Columns should represent workflow state.
|
|
64
|
+
|
|
65
|
+
### 3. Decompose into actionable tasks
|
|
66
|
+
|
|
67
|
+
Each generated task should be:
|
|
68
|
+
|
|
69
|
+
- small enough to complete as one coherent unit of work
|
|
70
|
+
- specific enough that another agent or engineer can act on it later
|
|
71
|
+
- named by outcome, not by vague area labels
|
|
72
|
+
- independent where possible, but explicitly linked when not
|
|
73
|
+
|
|
74
|
+
Prefer imperative or outcome-focused names such as:
|
|
75
|
+
|
|
76
|
+
- `Define authentication flow`
|
|
77
|
+
- `Create database migration plan`
|
|
78
|
+
- `Document deployment rollback steps`
|
|
79
|
+
|
|
80
|
+
Avoid umbrella task names such as:
|
|
81
|
+
|
|
82
|
+
- `Backend`
|
|
83
|
+
- `Frontend work`
|
|
84
|
+
- `Finish project`
|
|
85
|
+
|
|
86
|
+
### 4. Model dependencies carefully
|
|
87
|
+
|
|
88
|
+
Use the relations section to encode sequencing.
|
|
89
|
+
|
|
90
|
+
Preferred relation types:
|
|
91
|
+
|
|
92
|
+
- `depends-on`: the current task cannot start until the referenced task is complete
|
|
93
|
+
- `blocks`: the current task prevents the referenced task from starting or finishing
|
|
94
|
+
|
|
95
|
+
Important rules:
|
|
96
|
+
|
|
97
|
+
- Use one direction per dependency edge. Do not create both `depends-on A` and `blocks B` for the same relationship unless the user explicitly asks for mirrored wording.
|
|
98
|
+
- Do not create self-references.
|
|
99
|
+
- Do not create cycles.
|
|
100
|
+
- Other relation types such as `duplicates` or `obsoletes` are allowed, but they are not scheduling dependencies.
|
|
101
|
+
|
|
102
|
+
Kanbn's gantt dependency logic only treats `depends-on` and `blocks` as dependency edges.
|
|
103
|
+
|
|
104
|
+
### 5. Write lean metadata
|
|
105
|
+
|
|
106
|
+
Only include metadata when it is grounded in the prompt or materially useful.
|
|
107
|
+
|
|
108
|
+
Safe defaults:
|
|
109
|
+
|
|
110
|
+
- omit YAML front matter entirely when no metadata is needed
|
|
111
|
+
- omit `created`, `updated`, `started`, `completed`, `progress`, `comments`, and `history`
|
|
112
|
+
- omit `assigned` unless the user supplied owners
|
|
113
|
+
- omit `due` unless the user supplied a deadline
|
|
114
|
+
|
|
115
|
+
Useful optional metadata:
|
|
116
|
+
|
|
117
|
+
- `tags` for epics, domains, or sizing labels
|
|
118
|
+
- custom fields only if the board already defines them
|
|
119
|
+
|
|
120
|
+
### 6. Place tasks in columns intentionally
|
|
121
|
+
|
|
122
|
+
For a fresh plan, use this default placement strategy unless the user specifies otherwise:
|
|
123
|
+
|
|
124
|
+
- put ready, near-term tasks with no unresolved prerequisites in `Todo`
|
|
125
|
+
- put deferred or dependency-blocked tasks in `Backlog`
|
|
126
|
+
- leave `In Progress` and `Done` empty unless the user explicitly says work has already started or finished
|
|
127
|
+
|
|
128
|
+
Do not fabricate active work.
|
|
129
|
+
|
|
130
|
+
### 7. Validate before finalising
|
|
131
|
+
|
|
132
|
+
Before presenting the plan, validate it if the environment allows.
|
|
133
|
+
|
|
134
|
+
Use these helper files from this skill when available:
|
|
135
|
+
|
|
136
|
+
- `references/index-structure.md`
|
|
137
|
+
- `references/task-structure.md`
|
|
138
|
+
- `references/planning-rules.md`
|
|
139
|
+
- `scripts/validate-kanbn.mjs`
|
|
140
|
+
- `scripts/check-dependency-cycles.mjs`
|
|
141
|
+
|
|
142
|
+
Validation sequence:
|
|
143
|
+
|
|
144
|
+
1. Ensure `.kanbn/index.md` uses valid headings and task links.
|
|
145
|
+
2. Ensure each task file has a valid title, description, and any reserved sections in the correct format.
|
|
146
|
+
3. Run the validate wrapper.
|
|
147
|
+
4. Run the dependency cycle checker.
|
|
148
|
+
5. Fix any structural or dependency issues before stopping.
|
|
149
|
+
|
|
150
|
+
## Output Standards
|
|
151
|
+
|
|
152
|
+
### Index requirements
|
|
153
|
+
|
|
154
|
+
The index must:
|
|
155
|
+
|
|
156
|
+
- contain exactly one level-1 project heading
|
|
157
|
+
- contain optional description text directly under the project heading
|
|
158
|
+
- define workflow columns with level-2 headings
|
|
159
|
+
- list task ids as markdown links to `tasks/<task-id>.md`
|
|
160
|
+
|
|
161
|
+
### Task requirements
|
|
162
|
+
|
|
163
|
+
Each task file must:
|
|
164
|
+
|
|
165
|
+
- live at `.kanbn/tasks/<task-id>.md`
|
|
166
|
+
- contain exactly one top-level heading with the human-readable task name
|
|
167
|
+
- include a description tailored to the task
|
|
168
|
+
- use reserved headings correctly when present: `Metadata`, `Sub-tasks`, `Relations`, `Comments`, `History`
|
|
169
|
+
|
|
170
|
+
Descriptions should usually include:
|
|
171
|
+
|
|
172
|
+
- the purpose of the task
|
|
173
|
+
- key scope boundaries
|
|
174
|
+
- expected deliverable or outcome
|
|
175
|
+
- acceptance criteria or completion signals when useful
|
|
176
|
+
|
|
177
|
+
### Task id rules
|
|
178
|
+
|
|
179
|
+
Use stable kebab-case ids derived from the task name.
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
|
|
183
|
+
- `Set up CI pipeline` -> `set-up-ci-pipeline`
|
|
184
|
+
- `Plan data retention policy` -> `plan-data-retention-policy`
|
|
185
|
+
|
|
186
|
+
## When Revising An Existing Board
|
|
187
|
+
|
|
188
|
+
If `.kanbn/` already exists:
|
|
189
|
+
|
|
190
|
+
- preserve the existing board structure unless the user asked to redesign it
|
|
191
|
+
- preserve existing custom fields and option conventions
|
|
192
|
+
- avoid rewriting unrelated tasks
|
|
193
|
+
- add or edit only the planning artifacts needed for the requested plan change
|
|
194
|
+
|
|
195
|
+
## References
|
|
196
|
+
|
|
197
|
+
Use these files as the source of truth while authoring:
|
|
198
|
+
|
|
199
|
+
- `references/index-structure.md` for `.kanbn/index.md`
|
|
200
|
+
- `references/task-structure.md` for `.kanbn/tasks/*.md`
|
|
201
|
+
- `references/planning-rules.md` for decomposition and dependency heuristics
|
|
202
|
+
|
|
203
|
+
## Success Criteria
|
|
204
|
+
|
|
205
|
+
This skill is successful when it produces a Kanbn board that:
|
|
206
|
+
|
|
207
|
+
- accurately reflects the requested project scope
|
|
208
|
+
- is structurally valid
|
|
209
|
+
- contains actionable tasks instead of vague placeholders
|
|
210
|
+
- captures dependency order without cycles
|
|
211
|
+
- does not stray into implementation work
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Kanbn Index Reference
|
|
2
|
+
|
|
3
|
+
This reference describes the structure expected for `.kanbn/index.md` when generating a project plan.
|
|
4
|
+
|
|
5
|
+
## Minimum Valid Shape
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
---
|
|
9
|
+
startedColumns:
|
|
10
|
+
- In Progress
|
|
11
|
+
completedColumns:
|
|
12
|
+
- Done
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Project Name
|
|
16
|
+
|
|
17
|
+
Short project summary and planning assumptions.
|
|
18
|
+
|
|
19
|
+
## Backlog
|
|
20
|
+
|
|
21
|
+
- [task-id-1](tasks/task-id-1.md)
|
|
22
|
+
- [task-id-2](tasks/task-id-2.md)
|
|
23
|
+
|
|
24
|
+
## Todo
|
|
25
|
+
|
|
26
|
+
- [task-id-3](tasks/task-id-3.md)
|
|
27
|
+
|
|
28
|
+
## In Progress
|
|
29
|
+
|
|
30
|
+
## Done
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
|
|
35
|
+
- The file may begin with YAML front matter.
|
|
36
|
+
- There must be exactly one level-1 heading for the project name.
|
|
37
|
+
- Text under the project heading is the project description.
|
|
38
|
+
- After the description, each level-2 heading defines a column.
|
|
39
|
+
- Each column should contain a markdown list of task links.
|
|
40
|
+
- Each task link should point to `tasks/<task-id>.md`.
|
|
41
|
+
- The link text should be the task id, not the human-readable title.
|
|
42
|
+
|
|
43
|
+
## Recommended Defaults
|
|
44
|
+
|
|
45
|
+
If the user does not specify a workflow, prefer:
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
startedColumns:
|
|
49
|
+
- In Progress
|
|
50
|
+
completedColumns:
|
|
51
|
+
- Done
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
And columns:
|
|
55
|
+
|
|
56
|
+
- `Backlog`
|
|
57
|
+
- `Todo`
|
|
58
|
+
- `In Progress`
|
|
59
|
+
- `Done`
|
|
60
|
+
|
|
61
|
+
## Placement Guidance
|
|
62
|
+
|
|
63
|
+
- Put tasks in `Todo` only if they are ready to start and not obviously blocked.
|
|
64
|
+
- Put blocked, later-phase, or lower-priority work in `Backlog`.
|
|
65
|
+
- Leave `In Progress` empty for a newly generated plan unless the prompt explicitly says work is underway.
|
|
66
|
+
- Leave `Done` empty unless the prompt explicitly references completed work.
|
|
67
|
+
|
|
68
|
+
## Options Worth Using
|
|
69
|
+
|
|
70
|
+
Only add options when they are meaningful.
|
|
71
|
+
|
|
72
|
+
Commonly useful options:
|
|
73
|
+
|
|
74
|
+
- `startedColumns`
|
|
75
|
+
- `completedColumns`
|
|
76
|
+
- `hiddenColumns`
|
|
77
|
+
- `taskWorkloadTags`
|
|
78
|
+
- `defaultTaskWorkload`
|
|
79
|
+
- `customFields`
|
|
80
|
+
|
|
81
|
+
Avoid adding large option blocks that the user did not ask for.
|
|
82
|
+
|
|
83
|
+
## Common Mistakes
|
|
84
|
+
|
|
85
|
+
- Using headings inside the project description.
|
|
86
|
+
- Listing bare task ids instead of markdown links.
|
|
87
|
+
- Linking to the wrong path, for example `./tasks/...` or absolute paths.
|
|
88
|
+
- Treating columns as epics or teams instead of workflow states.
|
|
89
|
+
- Populating `In Progress` or `Done` with speculative work states.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Planning Rules
|
|
2
|
+
|
|
3
|
+
Use these heuristics when turning a prompt into Kanbn planning artifacts.
|
|
4
|
+
|
|
5
|
+
## Decomposition Heuristics
|
|
6
|
+
|
|
7
|
+
- Split by deliverable, decision, integration point, or risk area.
|
|
8
|
+
- Prefer tasks that produce one observable planning outcome.
|
|
9
|
+
- Separate discovery work from execution work when the unknowns are material.
|
|
10
|
+
- Separate prerequisite setup from dependent feature work.
|
|
11
|
+
- Separate cross-cutting concerns such as security, observability, migration, or rollout planning when they would otherwise disappear into feature tasks.
|
|
12
|
+
|
|
13
|
+
## Task Granularity
|
|
14
|
+
|
|
15
|
+
Aim for tasks that are:
|
|
16
|
+
|
|
17
|
+
- independently understandable
|
|
18
|
+
- narrow enough to estimate or assign later
|
|
19
|
+
- broad enough to avoid an explosion of trivial tasks
|
|
20
|
+
|
|
21
|
+
If a task description needs multiple unrelated deliverables, split it.
|
|
22
|
+
|
|
23
|
+
## Dependency Rules
|
|
24
|
+
|
|
25
|
+
- Add dependencies only when order materially matters.
|
|
26
|
+
- Prefer `depends-on` over ad hoc wording because Kanbn already understands it for scheduling.
|
|
27
|
+
- Use `blocks` when it reads more naturally from the prerequisite task.
|
|
28
|
+
- Do not encode the same dependency twice in opposite directions unless the user wants mirrored relations.
|
|
29
|
+
- Avoid diamond-shaped dependency graphs when a simpler linear or fan-out structure is more accurate.
|
|
30
|
+
|
|
31
|
+
## Prioritisation Rules
|
|
32
|
+
|
|
33
|
+
- Put foundational work before feature-specific work.
|
|
34
|
+
- Put architecture and interface decisions before implementation-heavy downstream tasks.
|
|
35
|
+
- Put risk-reduction tasks earlier when they can invalidate later work.
|
|
36
|
+
- Keep optional enhancements, stretch goals, and nice-to-haves in `Backlog`.
|
|
37
|
+
|
|
38
|
+
## Naming Rules
|
|
39
|
+
|
|
40
|
+
- Use explicit outcome-oriented titles.
|
|
41
|
+
- Prefer verbs like `Define`, `Plan`, `Document`, `Design`, `Map`, `Specify`, `Assess`, `Prepare`.
|
|
42
|
+
- Avoid names that just restate a subsystem with no action.
|
|
43
|
+
|
|
44
|
+
## Assumption Rules
|
|
45
|
+
|
|
46
|
+
- Do not invent deadlines, owners, or progress.
|
|
47
|
+
- If the prompt leaves a critical planning choice unresolved, capture it as an assumption or an explicit planning task.
|
|
48
|
+
- If the board already exists, preserve its conventions unless the user asks to change them.
|
|
49
|
+
|
|
50
|
+
## Final Review Checklist
|
|
51
|
+
|
|
52
|
+
Before finalising a generated board, confirm:
|
|
53
|
+
|
|
54
|
+
- every task in the index has a corresponding task file
|
|
55
|
+
- every relation points to an existing task id
|
|
56
|
+
- the dependency graph has no cycles
|
|
57
|
+
- task descriptions are specific enough to guide later execution
|
|
58
|
+
- the board contains planning artifacts only, not implementation work
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Kanbn Task Reference
|
|
2
|
+
|
|
3
|
+
This reference describes the structure expected for task files in `.kanbn/tasks/`.
|
|
4
|
+
|
|
5
|
+
## Minimum Valid Shape
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
# Define authentication flow
|
|
9
|
+
|
|
10
|
+
Describe the scope of the task, the decisions to make, and the expected output.
|
|
11
|
+
|
|
12
|
+
## Sub-tasks
|
|
13
|
+
|
|
14
|
+
- [ ] Review requirements
|
|
15
|
+
- [ ] Document the proposed flow
|
|
16
|
+
|
|
17
|
+
## Relations
|
|
18
|
+
|
|
19
|
+
- [depends-on gather-auth-requirements](gather-auth-requirements.md)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## With Metadata
|
|
23
|
+
|
|
24
|
+
```markdown
|
|
25
|
+
---
|
|
26
|
+
tags:
|
|
27
|
+
- Auth
|
|
28
|
+
- Planning
|
|
29
|
+
due: 2026-08-01T00:00:00.000Z
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# Define authentication flow
|
|
33
|
+
|
|
34
|
+
Describe the scope of the task, the constraints, and the deliverable.
|
|
35
|
+
|
|
36
|
+
## Sub-tasks
|
|
37
|
+
|
|
38
|
+
- [ ] Map user journeys
|
|
39
|
+
- [ ] Compare session and token approaches
|
|
40
|
+
- [ ] Document recommendation
|
|
41
|
+
|
|
42
|
+
## Relations
|
|
43
|
+
|
|
44
|
+
- [depends-on gather-auth-requirements](gather-auth-requirements.md)
|
|
45
|
+
- [blocks design-login-ui](design-login-ui.md)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Rules
|
|
49
|
+
|
|
50
|
+
- File name: `<task-id>.md`
|
|
51
|
+
- File path: `.kanbn/tasks/<task-id>.md`
|
|
52
|
+
- The first level-1 heading is the task name.
|
|
53
|
+
- Content below the title is the task description.
|
|
54
|
+
- Reserved level-2 headings are:
|
|
55
|
+
- `Metadata`
|
|
56
|
+
- `Sub-tasks`
|
|
57
|
+
- `Relations`
|
|
58
|
+
- `Comments`
|
|
59
|
+
- `History`
|
|
60
|
+
|
|
61
|
+
Other headings are allowed inside the description, but use them sparingly.
|
|
62
|
+
|
|
63
|
+
## Description Guidance
|
|
64
|
+
|
|
65
|
+
Good task descriptions usually cover:
|
|
66
|
+
|
|
67
|
+
- what needs to be planned, decided, designed, or documented
|
|
68
|
+
- what is explicitly in scope
|
|
69
|
+
- what is out of scope when that boundary matters
|
|
70
|
+
- what output should exist when the task is complete
|
|
71
|
+
|
|
72
|
+
Useful patterns:
|
|
73
|
+
|
|
74
|
+
```markdown
|
|
75
|
+
# Plan deployment strategy
|
|
76
|
+
|
|
77
|
+
Define how the application will be deployed across environments.
|
|
78
|
+
|
|
79
|
+
## Deliverables
|
|
80
|
+
|
|
81
|
+
- Deployment approach for development, staging, and production
|
|
82
|
+
- Rollback approach
|
|
83
|
+
- Infrastructure assumptions
|
|
84
|
+
|
|
85
|
+
## Acceptance Criteria
|
|
86
|
+
|
|
87
|
+
- Target environments are named
|
|
88
|
+
- Release path is documented
|
|
89
|
+
- Rollback constraints are captured
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Metadata Guidance
|
|
93
|
+
|
|
94
|
+
Prefer omission over invention.
|
|
95
|
+
|
|
96
|
+
Usually omit:
|
|
97
|
+
|
|
98
|
+
- `created`
|
|
99
|
+
- `updated`
|
|
100
|
+
- `started`
|
|
101
|
+
- `completed`
|
|
102
|
+
- `progress`
|
|
103
|
+
- `assigned`
|
|
104
|
+
- `comments`
|
|
105
|
+
- `history`
|
|
106
|
+
|
|
107
|
+
Add metadata only when the user supplied it or when the board already relies on it.
|
|
108
|
+
|
|
109
|
+
Good uses of metadata:
|
|
110
|
+
|
|
111
|
+
- `tags` for epics, teams, domains, or sizing labels
|
|
112
|
+
- `due` for explicit deadlines
|
|
113
|
+
- custom fields already defined by the existing board
|
|
114
|
+
|
|
115
|
+
## Relations Guidance
|
|
116
|
+
|
|
117
|
+
Relation entries use markdown links.
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
|
|
121
|
+
```markdown
|
|
122
|
+
## Relations
|
|
123
|
+
|
|
124
|
+
- [depends-on plan-data-model](plan-data-model.md)
|
|
125
|
+
- [blocks define-api-contract](define-api-contract.md)
|
|
126
|
+
- [duplicates old-auth-plan](old-auth-plan.md)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Semantics:
|
|
130
|
+
|
|
131
|
+
- `depends-on X`: this task waits for `X`
|
|
132
|
+
- `blocks X`: this task is a prerequisite for `X`
|
|
133
|
+
- `duplicates` and similar non-dependency relations are informational only
|
|
134
|
+
|
|
135
|
+
Do not add both `depends-on X` and the mirrored inverse on the same task unless the user wants that phrasing explicitly.
|
|
136
|
+
|
|
137
|
+
## Common Mistakes
|
|
138
|
+
|
|
139
|
+
- Mismatching the file name and referenced task id.
|
|
140
|
+
- Using plain text instead of a markdown link in `## Relations`.
|
|
141
|
+
- Inventing timestamps, progress, or comments.
|
|
142
|
+
- Writing a task name that is too broad to act on.
|
|
143
|
+
- Creating a relation to a task file that does not exist.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import process from 'process';
|
|
6
|
+
|
|
7
|
+
function usage() {
|
|
8
|
+
console.error('Usage: node skills/kanbn-plan/scripts/check-dependency-cycles.mjs [project-root] [--json]');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
let projectRoot = process.cwd();
|
|
13
|
+
let json = false;
|
|
14
|
+
|
|
15
|
+
for (const arg of argv) {
|
|
16
|
+
if (arg === '--json') {
|
|
17
|
+
json = true;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (arg.startsWith('-')) {
|
|
22
|
+
usage();
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (projectRoot !== process.cwd()) {
|
|
27
|
+
usage();
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
projectRoot = arg;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
json,
|
|
36
|
+
projectRoot: path.resolve(projectRoot)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureKanbnFiles(projectRoot) {
|
|
41
|
+
const tasksPath = path.join(projectRoot, '.kanbn', 'tasks');
|
|
42
|
+
if (!fs.existsSync(tasksPath)) {
|
|
43
|
+
throw new Error(`No Kanbn tasks directory found at ${tasksPath}`);
|
|
44
|
+
}
|
|
45
|
+
return tasksPath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normaliseRelationType(type) {
|
|
49
|
+
return String(type || '').trim().toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getTaskIdFromHref(href) {
|
|
53
|
+
const fileName = path.basename(String(href || '').trim());
|
|
54
|
+
return fileName.endsWith('.md') ? fileName.slice(0, -3) : fileName;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractRelationsSection(markdown) {
|
|
58
|
+
const lines = markdown.split(/\r?\n/);
|
|
59
|
+
const sectionLines = [];
|
|
60
|
+
let inSection = false;
|
|
61
|
+
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (/^##\s+Relations\s*$/.test(line.trim())) {
|
|
64
|
+
inSection = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (inSection && /^##\s+/.test(line.trim())) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (inSection) {
|
|
73
|
+
sectionLines.push(line);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return sectionLines.join('\n').trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseRelations(markdown) {
|
|
81
|
+
const section = extractRelationsSection(markdown);
|
|
82
|
+
if (!section) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const relations = [];
|
|
87
|
+
const lines = section.split(/\r?\n/);
|
|
88
|
+
const relationPattern = /^-\s+\[([^\]]+)\]\(([^)]+)\)\s*$/;
|
|
89
|
+
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const match = line.trim().match(relationPattern);
|
|
92
|
+
if (!match) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const text = match[1].trim();
|
|
97
|
+
const href = match[2].trim();
|
|
98
|
+
const targetTaskId = getTaskIdFromHref(href);
|
|
99
|
+
const relationType = text.endsWith(targetTaskId)
|
|
100
|
+
? text.slice(0, text.length - targetTaskId.length).trim()
|
|
101
|
+
: text;
|
|
102
|
+
|
|
103
|
+
relations.push({
|
|
104
|
+
task: targetTaskId,
|
|
105
|
+
type: normaliseRelationType(relationType)
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return relations;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function loadTasks(tasksPath) {
|
|
113
|
+
const tasks = new Map();
|
|
114
|
+
|
|
115
|
+
for (const entry of fs.readdirSync(tasksPath, { withFileTypes: true })) {
|
|
116
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const taskId = entry.name.slice(0, -3);
|
|
121
|
+
const markdown = fs.readFileSync(path.join(tasksPath, entry.name), 'utf8');
|
|
122
|
+
tasks.set(taskId, {
|
|
123
|
+
id: taskId,
|
|
124
|
+
relations: parseRelations(markdown)
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return tasks;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildDependencyGraph(tasks) {
|
|
132
|
+
const graph = new Map();
|
|
133
|
+
const danglingReferences = [];
|
|
134
|
+
|
|
135
|
+
for (const taskId of tasks.keys()) {
|
|
136
|
+
graph.set(taskId, new Set());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const [taskId, task] of tasks.entries()) {
|
|
140
|
+
for (const relation of task.relations) {
|
|
141
|
+
if (!relation.task) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let fromId = null;
|
|
146
|
+
let toId = null;
|
|
147
|
+
|
|
148
|
+
if (relation.type === 'depends-on') {
|
|
149
|
+
fromId = relation.task;
|
|
150
|
+
toId = taskId;
|
|
151
|
+
} else if (relation.type === 'blocks') {
|
|
152
|
+
fromId = taskId;
|
|
153
|
+
toId = relation.task;
|
|
154
|
+
} else {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!tasks.has(fromId) || !tasks.has(toId)) {
|
|
159
|
+
danglingReferences.push({
|
|
160
|
+
from: taskId,
|
|
161
|
+
relationType: relation.type,
|
|
162
|
+
target: relation.task
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
graph.get(fromId).add(toId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { danglingReferences, graph };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function findCycles(graph) {
|
|
175
|
+
const visited = new Set();
|
|
176
|
+
const visiting = new Set();
|
|
177
|
+
const stack = [];
|
|
178
|
+
const cycles = [];
|
|
179
|
+
const seenCycleKeys = new Set();
|
|
180
|
+
|
|
181
|
+
function recordCycle(startNode) {
|
|
182
|
+
const startIndex = stack.indexOf(startNode);
|
|
183
|
+
if (startIndex === -1) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const cycle = stack.slice(startIndex).concat(startNode);
|
|
188
|
+
const uniqueNodes = cycle.slice(0, -1);
|
|
189
|
+
const canonicalStart = [...uniqueNodes].sort()[0];
|
|
190
|
+
const canonicalIndex = uniqueNodes.indexOf(canonicalStart);
|
|
191
|
+
const rotated = uniqueNodes.slice(canonicalIndex).concat(uniqueNodes.slice(0, canonicalIndex));
|
|
192
|
+
const cycleKey = rotated.join('>');
|
|
193
|
+
|
|
194
|
+
if (!seenCycleKeys.has(cycleKey)) {
|
|
195
|
+
seenCycleKeys.add(cycleKey);
|
|
196
|
+
cycles.push(rotated.concat(rotated[0]));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function visit(node) {
|
|
201
|
+
visited.add(node);
|
|
202
|
+
visiting.add(node);
|
|
203
|
+
stack.push(node);
|
|
204
|
+
|
|
205
|
+
for (const nextNode of graph.get(node) || []) {
|
|
206
|
+
if (!visited.has(nextNode)) {
|
|
207
|
+
visit(nextNode);
|
|
208
|
+
} else if (visiting.has(nextNode)) {
|
|
209
|
+
recordCycle(nextNode);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
stack.pop();
|
|
214
|
+
visiting.delete(node);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const node of graph.keys()) {
|
|
218
|
+
if (!visited.has(node)) {
|
|
219
|
+
visit(node);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return cycles;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function printTextReport(result) {
|
|
227
|
+
if (result.danglingReferences.length > 0) {
|
|
228
|
+
console.error(`Dangling dependency references: ${result.danglingReferences.length}`);
|
|
229
|
+
for (const reference of result.danglingReferences) {
|
|
230
|
+
console.error(`- ${reference.from}: ${reference.relationType} ${reference.target}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (result.cycles.length > 0) {
|
|
235
|
+
console.error(`Dependency cycles detected: ${result.cycles.length}`);
|
|
236
|
+
for (const cycle of result.cycles) {
|
|
237
|
+
console.error(`- ${cycle.join(' -> ')}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (result.danglingReferences.length === 0 && result.cycles.length === 0) {
|
|
242
|
+
console.log('No dependency cycles or dangling dependency references found');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function main() {
|
|
247
|
+
const { json, projectRoot } = parseArgs(process.argv.slice(2));
|
|
248
|
+
|
|
249
|
+
let tasksPath;
|
|
250
|
+
try {
|
|
251
|
+
tasksPath = ensureKanbnFiles(projectRoot);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(error.message);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const tasks = loadTasks(tasksPath);
|
|
258
|
+
const { graph, danglingReferences } = buildDependencyGraph(tasks);
|
|
259
|
+
const cycles = findCycles(graph);
|
|
260
|
+
const result = { cycles, danglingReferences };
|
|
261
|
+
|
|
262
|
+
if (json) {
|
|
263
|
+
console.log(JSON.stringify(result, null, 2));
|
|
264
|
+
} else {
|
|
265
|
+
printTextReport(result);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (cycles.length > 0 || danglingReferences.length > 0) {
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
main();
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import process from 'process';
|
|
6
|
+
import { spawnSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
function usage() {
|
|
9
|
+
console.error('Usage: node skills/kanbn-plan/scripts/validate-kanbn.mjs [project-root]');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolveProjectRoot(argv) {
|
|
13
|
+
if (argv.length > 1) {
|
|
14
|
+
usage();
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return path.resolve(argv[0] || process.cwd());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureKanbnFiles(projectRoot) {
|
|
22
|
+
const indexPath = path.join(projectRoot, '.kanbn', 'index.md');
|
|
23
|
+
const tasksPath = path.join(projectRoot, '.kanbn', 'tasks');
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(indexPath)) {
|
|
26
|
+
console.error(`No Kanbn index found at ${indexPath}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(tasksPath)) {
|
|
31
|
+
console.error(`No Kanbn tasks directory found at ${tasksPath}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getCandidates(projectRoot) {
|
|
37
|
+
const candidates = [];
|
|
38
|
+
const localBin = path.join(projectRoot, 'node_modules', '.bin', 'kanbn');
|
|
39
|
+
|
|
40
|
+
if (process.env.KANBN_BIN) {
|
|
41
|
+
candidates.push({ command: process.env.KANBN_BIN, args: [], label: process.env.KANBN_BIN });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (fs.existsSync(localBin)) {
|
|
45
|
+
candidates.push({ command: localBin, args: [], label: localBin });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
candidates.push({ command: 'kanbn', args: [], label: 'kanbn' });
|
|
49
|
+
candidates.push({ command: 'npx', args: ['-y', '@basementuniverse/kanbn'], label: 'npx -y @basementuniverse/kanbn' });
|
|
50
|
+
|
|
51
|
+
return candidates;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runValidate(projectRoot) {
|
|
55
|
+
const candidates = getCandidates(projectRoot);
|
|
56
|
+
let lastError = null;
|
|
57
|
+
|
|
58
|
+
for (const candidate of candidates) {
|
|
59
|
+
const result = spawnSync(
|
|
60
|
+
candidate.command,
|
|
61
|
+
[...candidate.args, 'validate', '--json'],
|
|
62
|
+
{
|
|
63
|
+
cwd: projectRoot,
|
|
64
|
+
encoding: 'utf8'
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (result.error && result.error.code === 'ENOENT') {
|
|
69
|
+
lastError = result.error;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { candidate, result };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw lastError || new Error('Unable to locate a runnable Kanbn CLI');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractValidationErrors(output) {
|
|
80
|
+
const start = output.indexOf('[');
|
|
81
|
+
const end = output.lastIndexOf(']');
|
|
82
|
+
|
|
83
|
+
if (start === -1 || end === -1 || end < start) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(output.slice(start, end + 1));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function main() {
|
|
95
|
+
const projectRoot = resolveProjectRoot(process.argv.slice(2));
|
|
96
|
+
ensureKanbnFiles(projectRoot);
|
|
97
|
+
|
|
98
|
+
let execution;
|
|
99
|
+
try {
|
|
100
|
+
execution = runValidate(projectRoot);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(`Unable to run Kanbn validation: ${error.message}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { candidate, result } = execution;
|
|
107
|
+
const combinedOutput = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
108
|
+
|
|
109
|
+
if (result.status === 0) {
|
|
110
|
+
if (combinedOutput) {
|
|
111
|
+
console.log(combinedOutput);
|
|
112
|
+
} else {
|
|
113
|
+
console.log(`Kanbn validation passed via ${candidate.label}`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const errors = extractValidationErrors(combinedOutput);
|
|
119
|
+
if (errors) {
|
|
120
|
+
console.error(`Kanbn validation reported ${errors.length} error(s) via ${candidate.label}:`);
|
|
121
|
+
for (const error of errors) {
|
|
122
|
+
if (error && typeof error === 'object') {
|
|
123
|
+
console.error(JSON.stringify(error, null, 2));
|
|
124
|
+
} else {
|
|
125
|
+
console.error(String(error));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.error(combinedOutput || `Kanbn validation failed via ${candidate.label}`);
|
|
132
|
+
process.exit(result.status || 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main();
|