@basementuniverse/kanbn 1.0.0 → 1.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.
@@ -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();
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: kanbn-replan
3
+ description: Recalculate unfinished Kanbn task dates from the current project state, preserving completed tasks and writing updated scheduled dates back to existing task markdown files.
4
+ ---
5
+
6
+ # Kanbn Replan
7
+
8
+ Use this skill when a Kanbn board needs to be rescheduled after time has passed, work was paused, or the current Gantt chart shows that unfinished work no longer fits the available timeline.
9
+
10
+ Do not use this skill to redesign scope, rewrite completed tasks, or invent new task structure. Keep the workflow focused on rescheduling existing incomplete work.
11
+
12
+ ## Core Contract
13
+
14
+ Your output must stay inside the Kanbn task surface:
15
+
16
+ - `.kanbn/index.md`
17
+ - `.kanbn/tasks/*.md`
18
+
19
+ You may read the current board and gantt output, but the end result should be a revised schedule for existing tasks, not a new plan.
20
+
21
+ Do not modify completed tasks unless the user explicitly asks to reopen them. Leave completed task descriptions, relations, and completion history intact.
22
+
23
+ ## When to Use
24
+
25
+ Use this skill when the user wants to:
26
+
27
+ - move overdue work forward to a realistic schedule
28
+ - reschedule unfinished tasks after a long interruption
29
+ - spread work out to reduce overload
30
+ - refresh `started`, `plannedStart`, `plannedFinish`, and `due` dates for incomplete tasks
31
+ - inspect Gantt output and derive a revised schedule from it
32
+
33
+ ## Workflow
34
+
35
+ ### 1. Inspect the current board state
36
+
37
+ Gather the current planning context before making edits:
38
+
39
+ - current date
40
+ - tracked tasks and their columns
41
+ - task metadata relevant to scheduling (`started`, `plannedStart`, `plannedFinish`, `due`, `progress`, workload tags)
42
+ - dependency relations
43
+ - the current Gantt layout
44
+
45
+ Prefer `kanbn gantt -j` for the machine-readable schedule. If the replanning should be anchored to a specific point in time, pass `--now "date"` so the schedule is evaluated against the intended date.
46
+
47
+ ### 2. Decide what is in scope
48
+
49
+ Reschedule only incomplete work.
50
+
51
+ Treat these as locked unless the user says otherwise:
52
+
53
+ - tasks marked complete
54
+ - completed subtasks that should remain completed
55
+ - task names, descriptions, comments, and history
56
+ - dependency structure, unless the task graph itself is wrong and must be repaired to make the schedule valid
57
+
58
+ If a task is incomplete but already in progress, preserve evidence of work where that matters. Prefer updating scheduling metadata over rewriting task content.
59
+
60
+ ### 3. Rebuild the schedule
61
+
62
+ Use the gantt data and dependency order to decide new dates.
63
+
64
+ General rules:
65
+
66
+ - dependency order wins over wishful dates
67
+ - `plannedStart` should be the earliest preferred start floor when present
68
+ - `plannedFinish` should be the preferred finish target when present
69
+ - `started` should reflect when work actually begins or resumes
70
+ - `due` should reflect the target finish date for the current schedule
71
+ - if a task would otherwise overlap too much with adjacent work, shift it later rather than ignoring the overload
72
+ - if the user has returned after a long gap, treat incomplete tasks as candidates for re-baselining from the current date forward
73
+
74
+ When task size information is available, use it to distribute work more realistically. If exact sizing is missing, use the gantt layout, task relations, and existing metadata as the primary signals.
75
+
76
+ If `kanbn gantt -j` reports dependency cycles, stop and resolve or report them before rescheduling dependent tasks.
77
+
78
+ ### 4. Edit task files directly
79
+
80
+ Update the existing `.kanbn/tasks/*.md` files rather than creating a new board.
81
+
82
+ Prefer minimal metadata changes:
83
+
84
+ - keep task titles and descriptions stable
85
+ - update only the date fields needed to express the new plan
86
+ - preserve completed tasks exactly as they are
87
+ - avoid changing `created`, `completed`, comments, or history unless the user explicitly asks
88
+
89
+ If the task file format uses front matter or a `Metadata` section, keep it valid and consistent with the repo docs.
90
+
91
+ ### 5. Verify the result
92
+
93
+ After editing, validate the board:
94
+
95
+ 1. Run `kanbn validate`
96
+ 2. Re-run `kanbn gantt -j` to confirm the new dates produce a coherent schedule
97
+ 3. Check for obvious dependency or date regressions
98
+ 4. Fix any malformed task files before stopping
99
+
100
+ ## Scheduling Heuristics
101
+
102
+ Use these heuristics when the user has not specified a custom policy:
103
+
104
+ - keep completed tasks unchanged
105
+ - schedule tasks in dependency order
106
+ - move overdue unfinished tasks forward if they cannot realistically finish on their original due dates
107
+ - avoid piling every incomplete task onto the earliest possible date
108
+ - keep `due` dates aligned with the revised execution window
109
+ - if a task has no meaningful `started` date yet, set it when work is planned to resume rather than inventing a long-past start
110
+ - use workload tags and any existing custom scheduling fields as additional inputs when the board defines them
111
+
112
+ ## References
113
+
114
+ Use the repo docs as the source of truth for the data model and commands:
115
+
116
+ - `docs/commands/gantt.txt`
117
+ - `docs/commands/edit.txt`
118
+ - `docs/commands/validate.txt`
119
+ - `docs/task-structure.md`
120
+ - `docs/index-structure.md`
121
+