kanbango 3.6.2 → 5.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/kanbango.md DELETED
@@ -1,48 +0,0 @@
1
- # kanbango — Rename + Migrate Tool Plan
2
-
3
- ## Phase 1 — Add `migrate` command to `kanban.js`
4
-
5
- **New exported function `migrateAll(options)`** in `kanban.js`:
6
- - Walks all 4 columns
7
- - Finds `.md` files, reads via existing `parseMarkdownTask`, writes `.json` via `writeTask`
8
- - `writeTask` already deletes the old `.md` file automatically
9
- - `options.dryRun` — preview only, returns what would migrate
10
- - Returns `{ migrated: [{id, from, to}], errors: [{file, reason}] }`
11
-
12
- ## Phase 2 — Add `kanban migrate` CLI subcommand
13
-
14
- In `bin/kanban.js`:
15
- - `kanban migrate` — runs migration, reports each file converted
16
- - `kanban migrate --dry-run` — preview only
17
-
18
- ## Phase 3 — Rename everything from `markdown-kanban` → `kanbango`
19
-
20
- **Files to update:**
21
-
22
- | File | Changes |
23
- |------|---------|
24
- | `package.json` | name → `kanbango`, description → "JSON-first local Kanban board with web GUI, CLI, and MCP server", repo/homepage/bugs URLs → `k0r81/kanbango` |
25
- | `CHANGELOG.md` | Add `[2.0.0]` entry, rename references |
26
- | `bin/kanban.js` | `claudeMcpConfig` and `openCodeMcpConfig` — change `npx markdown-kanban` → `npx kanbango` (lines 26, 42) |
27
- | `README.md` | All `markdown-kanban` → `kanbango`, update description |
28
- | `LLM_AGENTS.md` | Same renames |
29
- | `AGENTS.md` | Same renames |
30
- | `.npmignore` | Check if any path references need updating |
31
-
32
- **What stays the same:**
33
- - CLI binary names: `kanban` and `kanban-cmd` — no change
34
- - `backlog/` directory structure
35
- - All internal logic in `kanban.js`, `mcp-server.js`, `index.js`
36
-
37
- ## Phase 4 — Tag & Publish
38
-
39
- 1. Bump version to `2.0.0` in `package.json`
40
- 2. `git tag v2.0.0 -m "kanbango: rename + migrate tool"`
41
- 3. Push tag to GitHub
42
- 4. Rename GitHub repo `k0r81/markdown-kanban` → `k0r81/kanbango` (manual, via GitHub UI)
43
- 5. `npm publish` — first publish as `kanbango`
44
-
45
- ## Phase 5 — Verify
46
-
47
- - `npm search kanbango` to confirm it's live
48
- - Check download page at `https://www.npmjs.com/package/kanbango`
package/planv2.md DELETED
@@ -1,317 +0,0 @@
1
- # Plan V2
2
-
3
- ## Goal
4
-
5
- Evolve `markdown-kanban` into an LLM-first task system with:
6
-
7
- - rich task and subtask descriptions
8
- - minimal token usage in MCP flows
9
- - a minimal MCP surface area
10
- - structured, actionable error messages
11
- - partial reads so agents only fetch the task slices they need
12
-
13
- ## Core Decision
14
-
15
- Use JSON as the canonical task storage format.
16
-
17
- Reasoning:
18
-
19
- - Markdown is human-friendly, but becomes fragile once tasks need nested rich fields.
20
- - JSON is easier to validate, partially read, partially update, and return through MCP.
21
- - Token savings will come primarily from response shaping in MCP, not from the on-disk format itself.
22
-
23
- ## Token Strategy
24
-
25
- The main token optimization should happen in MCP responses, not by compressing field names or over-optimizing file syntax.
26
-
27
- Avoid:
28
-
29
- - always returning the full task
30
- - adding many narrow MCP methods
31
- - cryptic short keys like `sp`, `ac`, `d`
32
-
33
- Prefer:
34
-
35
- - one read tool with multiple views
36
- - explicit field selection
37
- - compact summary responses by default
38
- - structured update responses with configurable return payloads
39
-
40
- ## Recommended Task Schema
41
-
42
- ```json
43
- {
44
- "id": "014",
45
- "title": "Google Calendar Integration",
46
- "column": "active",
47
- "epic_group": "Phase 1",
48
- "created": "2026-07-07",
49
- "description": "High-level context and implementation plan in markdown.",
50
- "specs": "Technical constraints, APIs, data model, edge cases.",
51
- "in_scope": [
52
- "OAuth connect/disconnect",
53
- "Two-way event sync"
54
- ],
55
- "out_of_scope": [
56
- "Microsoft Calendar",
57
- "Mobile push notifications"
58
- ],
59
- "acceptance_criteria": [
60
- "User can connect Google account",
61
- "Events sync in both directions",
62
- "Sync conflicts are logged"
63
- ],
64
- "subtasks": [
65
- {
66
- "id": "st-1",
67
- "text": "Implement OAuth flow",
68
- "done": false,
69
- "description": "Use PKCE, store refresh token encrypted."
70
- }
71
- ],
72
- "notes": "Optional freeform notes"
73
- }
74
- ```
75
-
76
- ## Field Semantics
77
-
78
- - `description`: the main context, why the work exists, and the implementation plan
79
- - `specs`: concrete technical facts, constraints, APIs, data shape, edge cases
80
- - `in_scope`: what this task includes (boundaries)
81
- - `out_of_scope`: explicit non-goals / exclusions
82
- - `acceptance_criteria`: what must be true for the task to count as done
83
- - `subtasks[].description`: local execution detail for a subtask
84
- - `notes`: optional freeform leftovers, references, or observations
85
-
86
- This split is useful for both humans and agents.
87
-
88
- ## MCP Surface
89
-
90
- Keep the MCP surface at 3 tools:
91
-
92
- - `kanban_read`
93
- - `kanban_create`
94
- - `kanban_update`
95
-
96
- Do not add separate tools like:
97
-
98
- - `kanban_read_specs`
99
- - `kanban_read_ac`
100
- - `kanban_read_subtasks`
101
-
102
- That would increase surface area without meaningfully reducing tokens.
103
-
104
- ## kanban_read Design
105
-
106
- Add support for response shaping.
107
-
108
- ### Option A: view presets
109
-
110
- ```json
111
- {
112
- "operation": "show",
113
- "task_id": "014",
114
- "view": "planning"
115
- }
116
- ```
117
-
118
- Suggested views:
119
-
120
- - `summary`: id, title, column, epic, created, progress counts
121
- - `planning`: summary + description + specs + in_scope + out_of_scope + acceptance_criteria
122
- - `execution`: planning + subtasks
123
- - `full`: everything including notes and metadata
124
-
125
- ### Option B: explicit fields
126
-
127
- ```json
128
- {
129
- "operation": "show",
130
- "task_id": "014",
131
- "fields": ["title", "description", "acceptance_criteria"]
132
- }
133
- ```
134
-
135
- Recommendation: support both.
136
-
137
- - `view` is simple and ergonomic
138
- - `fields` is precise for advanced agents
139
-
140
- If both are supplied, `fields` should win.
141
-
142
- ## kanban_create Design
143
-
144
- Allow rich fields during creation, but keep minimal creation possible.
145
-
146
- Minimal create:
147
-
148
- ```json
149
- {
150
- "title": "Add user authentication"
151
- }
152
- ```
153
-
154
- Full create:
155
-
156
- ```json
157
- {
158
- "title": "Add user authentication",
159
- "col": "planned",
160
- "epic": "Auth",
161
- "description": "Implement auth flow and session model.",
162
- "specs": "Use OAuth + session cookies.",
163
- "acceptance_criteria": [
164
- "User can sign in",
165
- "Session persists after refresh"
166
- ],
167
- "subtasks": [
168
- {
169
- "text": "Create auth routes",
170
- "description": "Add login and callback handlers."
171
- }
172
- ],
173
- "notes": "Optional"
174
- }
175
- ```
176
-
177
- ## kanban_update Design
178
-
179
- Use a patch-like payload and let callers control the returned payload size.
180
-
181
- ```json
182
- {
183
- "operation": "update",
184
- "task_id": "014",
185
- "patch": {
186
- "description": "Updated implementation plan...",
187
- "acceptance_criteria": [
188
- "OAuth works",
189
- "Tokens refresh correctly"
190
- ]
191
- },
192
- "return": "summary"
193
- }
194
- ```
195
-
196
- Suggested `return` values:
197
-
198
- - `none`
199
- - `summary`
200
- - `full`
201
-
202
- Default should be `summary`, not `full`.
203
-
204
- This avoids sending large descriptions back after every small edit.
205
-
206
- ## Read Strategy For Agents
207
-
208
- Agents should not always read the whole task.
209
-
210
- Recommended workflows:
211
-
212
- ### Triage
213
-
214
- Use `summary` only.
215
-
216
- ### Planning
217
-
218
- Read:
219
-
220
- - `description`
221
- - `specs`
222
- - `acceptance_criteria`
223
-
224
- ### Implementation
225
-
226
- Read:
227
-
228
- - planning fields
229
- - subtasks
230
-
231
- ### Verification
232
-
233
- Read:
234
-
235
- - `acceptance_criteria`
236
- - progress / status fields
237
-
238
- ### Backlog cleanup
239
-
240
- Read:
241
-
242
- - summary only
243
-
244
- This is not a bad idea. It is the correct way to reduce token use while staying useful.
245
-
246
- ## Error Design
247
-
248
- Errors should be structured JSON, not a plain string.
249
-
250
- Recommended shape:
251
-
252
- ```json
253
- {
254
- "error": {
255
- "code": "TASK_NOT_FOUND",
256
- "message": "Task 014 was not found",
257
- "hint": "Call kanban_read with operation=list to discover valid task ids",
258
- "details": {
259
- "task_id": "014"
260
- },
261
- "retryable": false
262
- }
263
- }
264
- ```
265
-
266
- Suggested error codes:
267
-
268
- - `TASK_NOT_FOUND`
269
- - `INVALID_COLUMN`
270
- - `INVALID_SUBTASK_INDEX`
271
- - `VALIDATION_ERROR`
272
- - `MISSING_REQUIRED_FIELD`
273
- - `TASK_CONFLICT`
274
- - `PARSE_ERROR`
275
-
276
- Good errors should help an agent recover without extra back-and-forth.
277
-
278
- ## Migration Strategy
279
-
280
- Existing Markdown files are already persisted data, so backward compatibility matters during migration.
281
-
282
- Recommended migration plan:
283
-
284
- 1. Add JSON schema support.
285
- 2. Support reading both `.md` and `.json` task files during transition.
286
- 3. Write new tasks as JSON.
287
- 4. Add an optional migration command later if needed.
288
- 5. Once the repo is clean and the migration is accepted, decide whether to keep Markdown read support permanently.
289
-
290
- ## Suggested Defaults
291
-
292
- - Canonical storage: JSON
293
- - MCP tools: keep exactly 3
294
- - Default read view: `summary` for lists, `planning` or `summary` for show depending on client choice
295
- - Default update return payload: `summary`
296
- - Rich task fields: `description`, `specs`, `acceptance_criteria`, `notes`
297
- - Rich subtask fields: `text`, `done`, `description`
298
-
299
- ## Non-Goals
300
-
301
- Avoid for now:
302
-
303
- - many specialized MCP methods
304
- - abbreviated schema keys just to save a few tokens
305
- - always rendering HTML for descriptions
306
- - over-structuring subtasks with too many fields
307
-
308
- ## Next Implementation Steps
309
-
310
- 1. Define the JSON schema in code and docs.
311
- 2. Update `kanban.js` to read and write JSON tasks.
312
- 3. Add transitional support for reading existing Markdown tasks.
313
- 4. Extend `kanban_read` with `view` and `fields`.
314
- 5. Extend `kanban_create` and `kanban_update` with rich fields and `patch` semantics.
315
- 6. Add structured MCP error responses with codes and hints.
316
- 7. Update the web GUI to edit `description`, `specs`, `acceptance_criteria`, and subtask descriptions.
317
- 8. Add tests for partial reads, structured errors, and Markdown-to-JSON compatibility.
@@ -1,69 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Fake opencode runner for workflow-gates tests.
4
- * Env:
5
- * FAKE_OPENCODE_MODE = pass|fail|blocked|gate_pass|gate_fail|gate_missing|hang|exit1
6
- * FAKE_OPENCODE_REVIEW_MODE = gate_pass|gate_fail|gate_missing (optional; overrides when --agent is review)
7
- * FAKE_OPENCODE_ARGV_FILE = path to write JSON argv
8
- * FAKE_OPENCODE_HANG_MS = hang duration (default 60000)
9
- */
10
- const fs = require('fs');
11
- const path = require('path');
12
-
13
- const args = process.argv.slice(2);
14
- const agentIdx = args.indexOf('--agent');
15
- const agent = agentIdx >= 0 ? args[agentIdx + 1] : '';
16
-
17
- let mode = String(process.env.FAKE_OPENCODE_MODE || 'pass').trim();
18
- const reviewMode = String(process.env.FAKE_OPENCODE_REVIEW_MODE || '').trim();
19
- // Review agents: prefer review mode, else map testing modes to gate_* defaults.
20
- if (agent === 'temida' || agent === 'review' || /temida|review/i.test(agent)) {
21
- if (reviewMode) {
22
- mode = reviewMode;
23
- } else if (mode === 'pass') {
24
- mode = 'gate_pass';
25
- } else if (mode === 'fail') {
26
- mode = 'gate_fail';
27
- } else if (mode === 'blocked' || mode === 'gate_missing') {
28
- mode = mode === 'blocked' ? 'gate_missing' : mode;
29
- }
30
- }
31
-
32
- const argvFile = process.env.FAKE_OPENCODE_ARGV_FILE;
33
- if (argvFile) {
34
- try {
35
- fs.mkdirSync(path.dirname(argvFile), { recursive: true });
36
- fs.writeFileSync(argvFile, JSON.stringify(args, null, 2) + '\n', 'utf-8');
37
- } catch (err) {
38
- process.stderr.write(`argv write failed: ${err.message}\n`);
39
- }
40
- }
41
-
42
- if (mode === 'hang') {
43
- const ms = Number(process.env.FAKE_OPENCODE_HANG_MS || 60000);
44
- setTimeout(() => process.exit(0), ms);
45
- return;
46
- }
47
-
48
- const outputs = {
49
- pass: { out: 'Tests green\nPASS\n', code: 0 },
50
- fail: { out: 'Tests red\nFAIL\n', code: 1 },
51
- blocked: { out: 'Missing env\nBLOCKED\n', code: 0 },
52
- gate_pass: {
53
- out: 'Nagroda: solidny diff.\nKara kosmetyczna: naming.\nWedka: ok.\nGATE: PASS\n',
54
- code: 0
55
- },
56
- gate_fail: {
57
- out: 'Kara: brak testow produkcji.\nWedka: dodaj testy.\nGATE: FAIL\n',
58
- code: 0
59
- },
60
- gate_missing: {
61
- out: 'Wyrok bez markera bramki.\nTylko narracja.\n',
62
- code: 0
63
- },
64
- exit1: { out: 'crashed without verdict\n', code: 1 }
65
- };
66
-
67
- const picked = outputs[mode] || outputs.pass;
68
- process.stdout.write(picked.out);
69
- process.exit(picked.code);
package/tests/index.js DELETED
@@ -1,19 +0,0 @@
1
- const assert = require('assert');
2
- const path = require('path');
3
-
4
- function run() {
5
- const pkg = require(path.join(__dirname, '..', 'index.js'));
6
- assert.ok(pkg.kanban, 'index exports kanban');
7
- assert.ok(pkg.plan, 'index exports plan');
8
- assert.ok(pkg.workflow, 'index exports workflow');
9
- assert.ok(pkg.guiRegistry, 'index exports guiRegistry');
10
- assert.ok(pkg.playbook, 'index exports playbook');
11
- assert.ok(pkg.kanban.COLS.includes('testing'));
12
- assert.ok(pkg.kanban.COLS.includes('review'));
13
- assert.strictEqual(typeof pkg.workflow.loadConfig, 'function');
14
- assert.strictEqual(typeof pkg.workflow.maybeEnqueueOnColumnEnter, 'function');
15
- assert.strictEqual(typeof pkg.plan.done, 'function');
16
- console.log('✓ index.test.js passed');
17
- }
18
-
19
- run();
@@ -1,118 +0,0 @@
1
- const assert = require('assert');
2
- const crypto = require('crypto');
3
- const fs = require('fs').promises;
4
- const os = require('os');
5
- const path = require('path');
6
- const { spawnSync } = require('child_process');
7
-
8
- const PKG = path.join(__dirname, '..');
9
- const CLI = path.join(PKG, 'bin', 'kanban.js');
10
- const SRC_QA = path.join(PKG, 'agents', 'qa-tester.md');
11
-
12
- function runCli(cwd, args) {
13
- return spawnSync(process.execPath, [CLI, ...args], {
14
- cwd,
15
- encoding: 'utf-8'
16
- });
17
- }
18
-
19
- function sha256(content) {
20
- return crypto.createHash('sha256').update(content).digest('hex');
21
- }
22
-
23
- async function run() {
24
- // init creates testing/review dirs + README
25
- const root = await fs.mkdtemp(path.join(os.tmpdir(), 'kanbango-bin-'));
26
- const init = runCli(root, ['init']);
27
- assert.strictEqual(init.status, 0, init.stderr);
28
- for (const col of ['testing', 'review', 'active', 'planned', 'icebox', 'done']) {
29
- const st = await fs.stat(path.join(root, 'backlog', col));
30
- assert.ok(st.isDirectory(), col);
31
- }
32
- const readme = await fs.readFile(path.join(root, 'backlog', 'README.md'), 'utf-8');
33
- assert.ok(readme.includes('testing/'));
34
- assert.ok(readme.includes('review/'));
35
-
36
- // mcp-init --opencode copies agents + writes manifest
37
- const ocRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kanbango-bin-oc-'));
38
- const oc = runCli(ocRoot, ['mcp-init', '--opencode']);
39
- assert.strictEqual(oc.status, 0, oc.stderr);
40
- const agentDir = path.join(ocRoot, '.opencode', 'agent');
41
- const agents = await fs.readdir(agentDir);
42
- assert.ok(agents.includes('qa-tester.md'));
43
- assert.ok(agents.includes('temida.md'));
44
- assert.ok(agents.includes('.kanbango-agents.json'));
45
- const temida = await fs.readFile(path.join(agentDir, 'temida.md'), 'utf-8');
46
- assert.ok(temida.includes('GATE: PASS'));
47
-
48
- const srcQa = await fs.readFile(SRC_QA, 'utf-8');
49
- const srcHash = sha256(srcQa);
50
- const manifest1 = JSON.parse(await fs.readFile(path.join(agentDir, '.kanbango-agents.json'), 'utf-8'));
51
- assert.strictEqual(manifest1.agents['qa-tester.md'], srcHash);
52
-
53
- // second run, identical source → unchanged
54
- const againSame = runCli(ocRoot, ['mcp-init', '--opencode']);
55
- assert.strictEqual(againSame.status, 0);
56
- assert.ok(againSame.stdout.includes('Bez zmian') || againSame.stdout.includes('unchanged')
57
- || againSame.stdout.includes('Pominięto') === false);
58
- assert.ok(againSame.stdout.includes('Bez zmian .opencode/agent/qa-tester.md'));
59
- assert.strictEqual(await fs.readFile(path.join(agentDir, 'qa-tester.md'), 'utf-8'), srcQa);
60
-
61
- // local edit while manifest matches previous package hash → conflict, keep local
62
- await fs.writeFile(path.join(agentDir, 'qa-tester.md'), 'KEEP_LOCAL_EDIT', 'utf-8');
63
- const conflictRun = runCli(ocRoot, ['mcp-init', '--opencode']);
64
- assert.strictEqual(conflictRun.status, 0);
65
- assert.ok(
66
- conflictRun.stdout.includes('Konflikt') || conflictRun.stdout.includes('Pominięto'),
67
- `expected conflict/skip message, got: ${conflictRun.stdout}`
68
- );
69
- assert.strictEqual(
70
- await fs.readFile(path.join(agentDir, 'qa-tester.md'), 'utf-8'),
71
- 'KEEP_LOCAL_EDIT'
72
- );
73
-
74
- // clean older package version: dest matches recorded hash, source "new" → auto update
75
- // Simulate by writing dest = old body, manifest hash = old hash, then force-path via
76
- // restoring package content is fixed; instead write dest to old body with matching manifest.
77
- const oldBody = 'OLD_PACKAGE_VERSION\n';
78
- const oldHash = sha256(oldBody);
79
- await fs.writeFile(path.join(agentDir, 'qa-tester.md'), oldBody, 'utf-8');
80
- const man = JSON.parse(await fs.readFile(path.join(agentDir, '.kanbango-agents.json'), 'utf-8'));
81
- man.agents['qa-tester.md'] = oldHash;
82
- await fs.writeFile(
83
- path.join(agentDir, '.kanbango-agents.json'),
84
- JSON.stringify(man, null, 2) + '\n',
85
- 'utf-8'
86
- );
87
- const autoUpdate = runCli(ocRoot, ['mcp-init', '--opencode']);
88
- assert.strictEqual(autoUpdate.status, 0, autoUpdate.stderr);
89
- assert.ok(
90
- autoUpdate.stdout.includes('Zaktualizowano .opencode/agent/qa-tester.md'),
91
- autoUpdate.stdout
92
- );
93
- assert.strictEqual(await fs.readFile(path.join(agentDir, 'qa-tester.md'), 'utf-8'), srcQa);
94
- const manAfter = JSON.parse(await fs.readFile(path.join(agentDir, '.kanbango-agents.json'), 'utf-8'));
95
- assert.strictEqual(manAfter.agents['qa-tester.md'], srcHash);
96
-
97
- // --force overwrites local edit
98
- await fs.writeFile(path.join(agentDir, 'qa-tester.md'), 'FORCE_ME', 'utf-8');
99
- const forceRun = runCli(ocRoot, ['mcp-init', '--opencode', '--force']);
100
- assert.strictEqual(forceRun.status, 0);
101
- assert.strictEqual(await fs.readFile(path.join(agentDir, 'qa-tester.md'), 'utf-8'), srcQa);
102
-
103
- // --claude does not create .opencode/agent
104
- const claudeRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kanbango-bin-cl-'));
105
- const cl = runCli(claudeRoot, ['mcp-init', '--claude']);
106
- assert.strictEqual(cl.status, 0, cl.stderr);
107
- await assert.rejects(
108
- () => fs.access(path.join(claudeRoot, '.opencode', 'agent')),
109
- (err) => err.code === 'ENOENT'
110
- );
111
-
112
- console.log('✓ bin-kanban.test.js passed');
113
- }
114
-
115
- run().catch((err) => {
116
- console.error(err);
117
- process.exit(1);
118
- });
package/tests/kanban.js DELETED
@@ -1,104 +0,0 @@
1
- const assert = require('assert');
2
- const fs = require('fs').promises;
3
- const os = require('os');
4
- const path = require('path');
5
-
6
- async function run() {
7
- const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kanbango-kanban-'));
8
- process.chdir(tempRoot);
9
- const kanban = require(path.join(__dirname, '..', 'kanban.js'));
10
-
11
- assert.ok(kanban.COLS.includes('testing'));
12
- assert.ok(kanban.COLS.includes('review'));
13
- assert.strictEqual(kanban.STATUS_MAP.testing, 'testing');
14
- assert.strictEqual(kanban.STATUS_MAP.review, 'review');
15
-
16
- await kanban.ensureBacklogDir();
17
- for (const col of ['testing', 'review']) {
18
- const st = await fs.stat(path.join(tempRoot, 'backlog', col));
19
- assert.ok(st.isDirectory());
20
- }
21
-
22
- const ev = kanban.normalizeEvidence([{
23
- diff: 'old',
24
- test_command: 't',
25
- stdout: '',
26
- stderr: '',
27
- exit_code: 0,
28
- stage: 'testing',
29
- agent: 'qa-tester',
30
- verdict: 'pass',
31
- summary: 'ok'
32
- }]);
33
- assert.strictEqual(ev[0].stage, 'testing');
34
- assert.strictEqual(ev[0].verdict, 'pass');
35
-
36
- const wf = kanban.normalizeWorkflow({
37
- stage: 'review',
38
- status: 'running',
39
- agent: 'temida',
40
- run_id: 'run-x'
41
- });
42
- assert.strictEqual(wf.stage, 'review');
43
- assert.strictEqual(wf.status, 'running');
44
-
45
- const task = await kanban.doCreate('Kanban unit', 'active', '—', {});
46
- const moved = await kanban.updateTask(task.id, { column: 'testing' });
47
- assert.strictEqual(moved.column, 'testing');
48
-
49
- await assert.rejects(
50
- () => kanban.updateTask(task.id, { column: 'done' }),
51
- (err) => {
52
- assert.strictEqual(err.code, 'INVALID_TRANSITION');
53
- assert.deepStrictEqual(err.details.allowed_columns, ['active', 'review']);
54
- assert.strictEqual(err.details.from, 'testing');
55
- assert.strictEqual(err.details.to, 'done');
56
- assert.ok(err.hint.includes('active, review'));
57
- return true;
58
- }
59
- );
60
- assert.deepStrictEqual(kanban.allowedColumnsFrom('active'), ['planned', 'testing', 'icebox']);
61
- assert.deepStrictEqual(kanban.allowedColumnsFrom('review'), ['active', 'done']);
62
- kanban.validateTransition('active', 'testing', '001');
63
- kanban.validateTransition('testing', 'testing', '001');
64
-
65
- const appended = await kanban.updateTask(task.id, {
66
- appendEvidence: {
67
- diff: '',
68
- test_command: 'fake',
69
- stdout: 'PASS',
70
- stderr: '',
71
- exit_code: 0,
72
- stage: 'testing',
73
- agent: 'qa-tester',
74
- verdict: 'pass',
75
- summary: 'PASS'
76
- },
77
- workflow: {
78
- stage: 'testing',
79
- status: 'pass',
80
- agent: 'qa-tester',
81
- run_id: 'run-1'
82
- }
83
- });
84
- assert.ok(appended.evidence.some((e) => e.verdict === 'pass'));
85
- assert.strictEqual(appended.workflow.status, 'pass');
86
-
87
- assert.strictEqual(
88
- kanban.deriveEpicStatus([{ column: 'testing' }], {}),
89
- 'active'
90
- );
91
- const progress = kanban.getEpicProgress([
92
- { column: 'testing' },
93
- { column: 'review' }
94
- ]);
95
- assert.strictEqual(progress.tasks_testing, 1);
96
- assert.strictEqual(progress.tasks_review, 1);
97
-
98
- console.log('✓ kanban.test.js passed');
99
- }
100
-
101
- run().catch((err) => {
102
- console.error(err);
103
- process.exit(1);
104
- });