@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.
- package/README.md +2 -0
- package/docs/commands/gantt.txt +3 -7
- package/docs/commands/history.txt +29 -0
- package/docs/task-structure.md +8 -4
- package/example/.kanbn/index.md +13 -10
- package/example/.kanbn/tasks/build-invoice-download-endpoint.md +3 -1
- package/example/.kanbn/tasks/create-self-serve-cancellation-flow.md +1 -1
- package/example/.kanbn/tasks/optimize-dashboard-first-load.md +1 -1
- package/example/.kanbn/tasks/prototype-report-export-scheduler.md +1 -1
- package/package.json +1 -1
- package/routes/add.json +2 -0
- package/routes/edit.json +2 -0
- package/routes/find.json +2 -0
- package/routes/history.json +27 -0
- package/routes/sort.json +2 -0
- 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/skills/kanbn-replan/SKILL.md +121 -0
- package/src/controller/add.js +49 -17
- package/src/controller/edit.js +91 -24
- package/src/controller/find.js +20 -3
- package/src/controller/gantt.js +2 -3
- package/src/controller/history.js +105 -0
- package/src/controller/sort.js +11 -3
- package/src/main.js +176 -14
- package/src/parse-task.js +20 -6
|
@@ -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
|
+
|