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/workflow.js CHANGED
@@ -7,11 +7,25 @@ const BACKLOG = path.join(process.cwd(), 'backlog');
7
7
  const CONFIG_PATH = path.join(BACKLOG, 'kanbango.json');
8
8
  const JOBS_DIR = path.join(BACKLOG, '.workflow');
9
9
 
10
+ const GATE_COLUMNS = ['testing', 'review'];
11
+ const DEFAULT_LABELS = {
12
+ icebox: 'Icebox',
13
+ planned: 'Planned',
14
+ active: 'NOW',
15
+ testing: 'Testing',
16
+ review: 'Review',
17
+ done: 'Done'
18
+ };
19
+
10
20
  /** PIDs spawned by this process — only these may be SIGTERM'd on timeout. */
11
21
  const ownedPids = new Set();
12
22
  /** In-flight run_ids to prevent double-spawn within this process. */
13
23
  const runningLocal = new Set();
14
24
 
25
+ let cachedConfig = null;
26
+ let cacheCwd = null;
27
+ let ensureTail = Promise.resolve();
28
+
15
29
  function workflowError(code, message, hint, details = {}) {
16
30
  return kanban.createKanbanError(code, message, hint, details, false, 400);
17
31
  }
@@ -20,6 +34,85 @@ function isNonEmptyString(value) {
20
34
  return typeof value === 'string' && value.trim().length > 0;
21
35
  }
22
36
 
37
+ function parseAgentField(value, fieldName, defaultName) {
38
+ if (value === undefined) {
39
+ return { enabled: true, name: defaultName };
40
+ }
41
+ if (typeof value === 'string') {
42
+ if (!isNonEmptyString(value)) {
43
+ throw workflowError(
44
+ 'CONFIG_INVALID',
45
+ `workflow.${fieldName} must be a non-empty string when set`,
46
+ `Fix workflow.${fieldName} in backlog/kanbango.json`,
47
+ { field: `workflow.${fieldName}` }
48
+ );
49
+ }
50
+ return { enabled: true, name: value.trim() };
51
+ }
52
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
53
+ throw workflowError(
54
+ 'CONFIG_INVALID',
55
+ `workflow.${fieldName} must be a string or { enabled, name }`,
56
+ `Use a name string or object with optional enabled/name`,
57
+ { field: `workflow.${fieldName}` }
58
+ );
59
+ }
60
+ if (value.enabled !== undefined && typeof value.enabled !== 'boolean') {
61
+ throw workflowError(
62
+ 'CONFIG_INVALID',
63
+ `workflow.${fieldName}.enabled must be a boolean`,
64
+ 'Use true or false',
65
+ { field: `workflow.${fieldName}.enabled` }
66
+ );
67
+ }
68
+ if (value.name !== undefined && !isNonEmptyString(value.name)) {
69
+ throw workflowError(
70
+ 'CONFIG_INVALID',
71
+ `workflow.${fieldName}.name must be a non-empty string when set`,
72
+ `Fix workflow.${fieldName}.name in backlog/kanbango.json`,
73
+ { field: `workflow.${fieldName}.name` }
74
+ );
75
+ }
76
+ return {
77
+ enabled: value.enabled === undefined ? true : value.enabled,
78
+ name: isNonEmptyString(value.name) ? value.name.trim() : defaultName
79
+ };
80
+ }
81
+
82
+ function parseGateColumn(value, columnId) {
83
+ if (value === undefined) {
84
+ return { enabled: true, label: DEFAULT_LABELS[columnId] };
85
+ }
86
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
87
+ throw workflowError(
88
+ 'CONFIG_INVALID',
89
+ `columns.${columnId} must be an object`,
90
+ `Use { "enabled": true, "label": "${DEFAULT_LABELS[columnId]}" }`,
91
+ { field: `columns.${columnId}` }
92
+ );
93
+ }
94
+ if (value.enabled !== undefined && typeof value.enabled !== 'boolean') {
95
+ throw workflowError(
96
+ 'CONFIG_INVALID',
97
+ `columns.${columnId}.enabled must be a boolean`,
98
+ 'Use true or false',
99
+ { field: `columns.${columnId}.enabled` }
100
+ );
101
+ }
102
+ if (value.label !== undefined && !isNonEmptyString(value.label)) {
103
+ throw workflowError(
104
+ 'CONFIG_INVALID',
105
+ `columns.${columnId}.label must be a non-empty string when set`,
106
+ `Fix columns.${columnId}.label in backlog/kanbango.json`,
107
+ { field: `columns.${columnId}.label` }
108
+ );
109
+ }
110
+ return {
111
+ enabled: value.enabled === undefined ? true : value.enabled,
112
+ label: isNonEmptyString(value.label) ? value.label.trim() : DEFAULT_LABELS[columnId]
113
+ };
114
+ }
115
+
23
116
  function assertWorkflowShape(workflow) {
24
117
  if (!workflow || typeof workflow !== 'object' || Array.isArray(workflow)) {
25
118
  throw workflowError(
@@ -37,14 +130,23 @@ function assertWorkflowShape(workflow) {
37
130
  { field: 'workflow.enabled' }
38
131
  );
39
132
  }
40
- const stringFields = ['command', 'testing_agent', 'e2e_agent', 'review_agent'];
41
- for (const field of stringFields) {
42
- if (workflow[field] !== undefined && !isNonEmptyString(workflow[field])) {
133
+ if (workflow.command !== undefined && !isNonEmptyString(workflow.command)) {
134
+ throw workflowError(
135
+ 'CONFIG_INVALID',
136
+ 'workflow.command must be a non-empty string when set',
137
+ 'Fix workflow.command in backlog/kanbango.json',
138
+ { field: 'workflow.command' }
139
+ );
140
+ }
141
+ if (workflow.e2e_agent !== undefined && !isNonEmptyString(workflow.e2e_agent)
142
+ && !(workflow.e2e_agent && typeof workflow.e2e_agent === 'object')) {
143
+ // Keep legacy string validation; object form not used for spawn yet.
144
+ if (typeof workflow.e2e_agent === 'string' && !isNonEmptyString(workflow.e2e_agent)) {
43
145
  throw workflowError(
44
146
  'CONFIG_INVALID',
45
- `workflow.${field} must be a non-empty string when set`,
46
- `Fix workflow.${field} in backlog/kanbango.json`,
47
- { field: `workflow.${field}` }
147
+ 'workflow.e2e_agent must be a non-empty string when set',
148
+ 'Fix workflow.e2e_agent in backlog/kanbango.json',
149
+ { field: 'workflow.e2e_agent' }
48
150
  );
49
151
  }
50
152
  }
@@ -58,6 +160,130 @@ function assertWorkflowShape(workflow) {
58
160
  );
59
161
  }
60
162
  }
163
+ // Validate agent fields early (parseAgentField throws CONFIG_INVALID).
164
+ parseAgentField(workflow.testing_agent, 'testing_agent', 'qa-tester');
165
+ parseAgentField(workflow.review_agent, 'review_agent', 'temida');
166
+ if (workflow.e2e_agent !== undefined && typeof workflow.e2e_agent === 'string') {
167
+ parseAgentField(workflow.e2e_agent, 'e2e_agent', 'qa-e2e-tester');
168
+ }
169
+ }
170
+
171
+ function assertColumnsShape(columns) {
172
+ if (columns === undefined) return;
173
+ if (!columns || typeof columns !== 'object' || Array.isArray(columns)) {
174
+ throw workflowError(
175
+ 'CONFIG_INVALID',
176
+ 'columns config must be an object',
177
+ 'Use { "testing": { "enabled": true, "label": "Testing" }, "review": { ... } }',
178
+ { field: 'columns' }
179
+ );
180
+ }
181
+ for (const key of Object.keys(columns)) {
182
+ if (!GATE_COLUMNS.includes(key)) {
183
+ throw workflowError(
184
+ 'CONFIG_INVALID',
185
+ `columns.${key} is not configurable`,
186
+ 'Only testing and review may be set under columns',
187
+ { field: `columns.${key}`, allowed: GATE_COLUMNS }
188
+ );
189
+ }
190
+ }
191
+ parseGateColumn(columns.testing, 'testing');
192
+ parseGateColumn(columns.review, 'review');
193
+ }
194
+
195
+ function buildTransitions(testingOn, reviewOn) {
196
+ const afterActive = testingOn ? 'testing' : (reviewOn ? 'review' : 'done');
197
+ const afterTesting = reviewOn ? 'review' : 'done';
198
+
199
+ const transitions = {
200
+ icebox: ['planned'],
201
+ planned: ['active', 'icebox', afterActive],
202
+ active: ['planned', afterActive, 'icebox'],
203
+ done: ['active']
204
+ };
205
+
206
+ // Dedupe while preserving order
207
+ for (const key of Object.keys(transitions)) {
208
+ transitions[key] = [...new Set(transitions[key])];
209
+ }
210
+
211
+ if (testingOn) {
212
+ transitions.testing = ['active', afterTesting];
213
+ }
214
+ if (reviewOn) {
215
+ transitions.review = ['active', 'done'];
216
+ }
217
+
218
+ return transitions;
219
+ }
220
+
221
+ function buildActiveCols(testingOn, reviewOn) {
222
+ const cols = ['active', 'planned', 'icebox'];
223
+ if (testingOn) cols.push('testing');
224
+ if (reviewOn) cols.push('review');
225
+ cols.push('done');
226
+ return cols;
227
+ }
228
+
229
+ function planDoneTarget(testingOn, reviewOn) {
230
+ if (testingOn) return 'testing';
231
+ if (reviewOn) return 'review';
232
+ return 'done';
233
+ }
234
+
235
+ function defaultConfig() {
236
+ const testingOn = false;
237
+ const reviewOn = false;
238
+ return {
239
+ missing: true,
240
+ enabled: false,
241
+ command: 'opencode',
242
+ testing_agent: { enabled: false, name: 'qa-tester' },
243
+ e2e_agent: { enabled: false, name: 'qa-e2e-tester' },
244
+ review_agent: { enabled: false, name: 'temida' },
245
+ timeout_ms: 600000,
246
+ columns: {
247
+ icebox: { enabled: true, label: DEFAULT_LABELS.icebox },
248
+ planned: { enabled: true, label: DEFAULT_LABELS.planned },
249
+ active: { enabled: true, label: DEFAULT_LABELS.active },
250
+ testing: { enabled: false, label: DEFAULT_LABELS.testing },
251
+ review: { enabled: false, label: DEFAULT_LABELS.review },
252
+ done: { enabled: true, label: DEFAULT_LABELS.done }
253
+ },
254
+ active_cols: buildActiveCols(testingOn, reviewOn),
255
+ workflow_stages: [],
256
+ transitions: buildTransitions(testingOn, reviewOn),
257
+ plan_done_column: planDoneTarget(testingOn, reviewOn),
258
+ move_order: ['icebox', 'planned', 'active', 'done'],
259
+ board_columns: [
260
+ { id: 'icebox', label: DEFAULT_LABELS.icebox },
261
+ { id: 'planned', label: DEFAULT_LABELS.planned },
262
+ { id: 'done', label: DEFAULT_LABELS.done }
263
+ ]
264
+ };
265
+ }
266
+
267
+ function shapePublicConfig(config) {
268
+ return {
269
+ columns: config.columns,
270
+ active_cols: config.active_cols.slice(),
271
+ workflow_stages: config.workflow_stages.slice(),
272
+ transitions: Object.fromEntries(
273
+ Object.entries(config.transitions).map(([k, v]) => [k, v.slice()])
274
+ ),
275
+ plan_done_column: config.plan_done_column,
276
+ move_order: config.move_order.slice(),
277
+ board_columns: config.board_columns.map((c) => ({ id: c.id, label: c.label })),
278
+ workflow: {
279
+ enabled: config.enabled,
280
+ command: config.command,
281
+ testing_agent: { ...config.testing_agent },
282
+ e2e_agent: { ...config.e2e_agent },
283
+ review_agent: { ...config.review_agent },
284
+ timeout_ms: config.timeout_ms
285
+ }
286
+ };
61
287
  }
62
288
 
63
289
  async function loadConfig() {
@@ -66,7 +292,7 @@ async function loadConfig() {
66
292
  raw = await fs.readFile(CONFIG_PATH, 'utf-8');
67
293
  } catch (error) {
68
294
  if (error.code === 'ENOENT') {
69
- return { enabled: false, missing: true };
295
+ return defaultConfig();
70
296
  }
71
297
  throw error;
72
298
  }
@@ -87,26 +313,121 @@ async function loadConfig() {
87
313
  throw workflowError(
88
314
  'CONFIG_INVALID',
89
315
  'kanbango.json root must be an object',
90
- 'Use { "workflow": { ... } }',
316
+ 'Use { "workflow": { ... }, "columns": { ... } }',
91
317
  { field: 'root' }
92
318
  );
93
319
  }
94
320
 
95
- assertWorkflowShape(parsed.workflow);
96
- const w = parsed.workflow;
321
+ assertColumnsShape(parsed.columns);
322
+
323
+ // workflow optional when only columns are configured
324
+ let w = parsed.workflow;
325
+ if (w === undefined) {
326
+ w = { enabled: false };
327
+ }
328
+ assertWorkflowShape(w);
329
+
330
+ const testingCol = parseGateColumn(parsed.columns && parsed.columns.testing, 'testing');
331
+ const reviewCol = parseGateColumn(parsed.columns && parsed.columns.review, 'review');
332
+ const testingOn = testingCol.enabled;
333
+ const reviewOn = reviewCol.enabled;
334
+
335
+ const testingAgent = parseAgentField(w.testing_agent, 'testing_agent', 'qa-tester');
336
+ const reviewAgent = parseAgentField(w.review_agent, 'review_agent', 'temida');
337
+ let e2eAgent = { enabled: true, name: 'qa-e2e-tester' };
338
+ if (w.e2e_agent !== undefined) {
339
+ if (typeof w.e2e_agent === 'string') {
340
+ e2eAgent = parseAgentField(w.e2e_agent, 'e2e_agent', 'qa-e2e-tester');
341
+ } else {
342
+ e2eAgent = parseAgentField(w.e2e_agent, 'e2e_agent', 'qa-e2e-tester');
343
+ }
344
+ }
345
+
97
346
  const envRunner = String(process.env.KANBANGO_WORKFLOW_RUNNER || '').trim();
347
+ const activeCols = buildActiveCols(testingOn, reviewOn);
348
+ const workflowStages = GATE_COLUMNS.filter((col) => (col === 'testing' ? testingOn : reviewOn));
349
+ const moveOrder = ['icebox', 'planned', 'active']
350
+ .concat(testingOn ? ['testing'] : [])
351
+ .concat(reviewOn ? ['review'] : [])
352
+ .concat(['done']);
353
+
354
+ const boardColumns = [
355
+ { id: 'icebox', label: DEFAULT_LABELS.icebox },
356
+ { id: 'planned', label: DEFAULT_LABELS.planned }
357
+ ];
358
+ if (testingOn) boardColumns.push({ id: 'testing', label: testingCol.label });
359
+ if (reviewOn) boardColumns.push({ id: 'review', label: reviewCol.label });
360
+ boardColumns.push({ id: 'done', label: DEFAULT_LABELS.done });
98
361
 
99
362
  return {
100
- enabled: w.enabled,
101
363
  missing: false,
364
+ enabled: w.enabled,
102
365
  command: envRunner || (isNonEmptyString(w.command) ? w.command.trim() : 'opencode'),
103
- testing_agent: isNonEmptyString(w.testing_agent) ? w.testing_agent.trim() : 'qa-tester',
104
- e2e_agent: isNonEmptyString(w.e2e_agent) ? w.e2e_agent.trim() : 'qa-e2e-tester',
105
- review_agent: isNonEmptyString(w.review_agent) ? w.review_agent.trim() : 'temida',
106
- timeout_ms: Number.isInteger(w.timeout_ms) && w.timeout_ms > 0 ? w.timeout_ms : 600000
366
+ testing_agent: testingAgent,
367
+ e2e_agent: e2eAgent,
368
+ review_agent: reviewAgent,
369
+ timeout_ms: Number.isInteger(w.timeout_ms) && w.timeout_ms > 0 ? w.timeout_ms : 600000,
370
+ columns: {
371
+ icebox: { enabled: true, label: DEFAULT_LABELS.icebox },
372
+ planned: { enabled: true, label: DEFAULT_LABELS.planned },
373
+ active: { enabled: true, label: DEFAULT_LABELS.active },
374
+ testing: testingCol,
375
+ review: reviewCol,
376
+ done: { enabled: true, label: DEFAULT_LABELS.done }
377
+ },
378
+ active_cols: activeCols,
379
+ workflow_stages: workflowStages,
380
+ transitions: buildTransitions(testingOn, reviewOn),
381
+ plan_done_column: planDoneTarget(testingOn, reviewOn),
382
+ move_order: moveOrder,
383
+ board_columns: boardColumns
107
384
  };
108
385
  }
109
386
 
387
+ async function migrateDisabledGateCards(config) {
388
+ const migrated = [];
389
+ for (const col of GATE_COLUMNS) {
390
+ if (config.columns[col].enabled) continue;
391
+ const tasks = await kanban.listTasksInKnownColumn(col);
392
+ for (const task of tasks) {
393
+ await kanban.relocateTask(task.id, 'planned');
394
+ migrated.push({ id: task.id, from: col, to: 'planned' });
395
+ }
396
+ }
397
+ return migrated;
398
+ }
399
+
400
+ /**
401
+ * Load board config, apply layout to kanban, migrate cards out of disabled gates.
402
+ * Always re-reads backlog/kanbango.json (file is small) so enable/disable changes apply.
403
+ * Migration is idempotent: second call finds no cards in disabled columns.
404
+ */
405
+ async function ensureBoardConfig({ force = false } = {}) {
406
+ const run = ensureTail.then(async () => {
407
+ const cwd = process.cwd();
408
+ // force kept for API/tests; always reload from disk so config edits are live.
409
+ void force;
410
+ const config = await loadConfig();
411
+ kanban.applyBoardLayout({
412
+ cols: config.active_cols,
413
+ transitions: config.transitions,
414
+ workflowStages: config.workflow_stages
415
+ });
416
+ await migrateDisabledGateCards(config);
417
+
418
+ cachedConfig = config;
419
+ cacheCwd = cwd;
420
+ return config;
421
+ });
422
+ ensureTail = run.then(() => undefined, () => undefined);
423
+ return run;
424
+ }
425
+
426
+ function clearConfigCache() {
427
+ cachedConfig = null;
428
+ cacheCwd = null;
429
+ }
430
+
110
431
  function newRunId() {
111
432
  return `run-${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`;
112
433
  }
@@ -139,9 +460,12 @@ function truncate(text, max = 2000) {
139
460
 
140
461
  function parseTestingVerdict(stdout, stderr, exitCode) {
141
462
  const text = `${stdout}\n${stderr}`;
142
- if (/\bBLOCKED\b/i.test(text)) return 'blocked';
143
- if (/\bPASS\b/i.test(text) && !/\bFAIL\b/i.test(text)) return 'pass';
144
- if (/\bFAIL\b/i.test(text)) return 'fail';
463
+ // Last standalone PASS|FAIL|BLOCKED wins — prompt text itself contains all three.
464
+ const matches = text.match(/(?:^|[^A-Za-z])(PASS|FAIL|BLOCKED)(?:[^A-Za-z]|$)/gi) || [];
465
+ const last = matches.length ? matches[matches.length - 1].replace(/[^A-Za-z]/g, '').toLowerCase() : '';
466
+ if (last === 'pass') return 'pass';
467
+ if (last === 'blocked') return 'blocked';
468
+ if (last === 'fail') return 'fail';
145
469
  if (exitCode !== 0) return 'fail';
146
470
  return 'fail';
147
471
  }
@@ -265,9 +589,11 @@ function outcomeForResult(stage, result) {
265
589
  if (stage === 'testing') {
266
590
  const verdict = parseTestingVerdict(result.stdout, result.stderr, result.exitCode);
267
591
  if (verdict === 'pass') {
592
+ const config = cachedConfig || defaultConfig();
593
+ const nextColumn = config.columns.review.enabled ? 'review' : 'done';
268
594
  return {
269
595
  verdict: 'pass',
270
- nextColumn: 'review',
596
+ nextColumn,
271
597
  workflowStatus: 'pass',
272
598
  summary: truncate(result.stdout || 'PASS', 500)
273
599
  };
@@ -356,7 +682,8 @@ async function finishRun(taskId, stage, agent, runId, result) {
356
682
  }
357
683
 
358
684
  async function startRun(task, stage, config) {
359
- const agent = agentForStage(config, stage);
685
+ const agentField = agentForStage(config, stage);
686
+ const agent = agentField.name;
360
687
  const runId = newRunId();
361
688
  const startedAt = new Date().toISOString();
362
689
 
@@ -411,11 +738,20 @@ async function maybeEnqueueOnColumnEnter(task, previousColumn) {
411
738
  return { enqueued: false, reason: 'same_column' };
412
739
  }
413
740
 
414
- const config = await loadConfig();
741
+ const config = await ensureBoardConfig();
415
742
  if (config.missing || !config.enabled) {
416
743
  return { enqueued: false, reason: config.missing ? 'no_config' : 'disabled' };
417
744
  }
418
745
 
746
+ if (!config.columns[task.column] || !config.columns[task.column].enabled) {
747
+ return { enqueued: false, reason: 'column_disabled' };
748
+ }
749
+
750
+ const agentField = agentForStage(config, task.column);
751
+ if (!agentField.enabled) {
752
+ return { enqueued: false, reason: 'agent_disabled' };
753
+ }
754
+
419
755
  // Dedup: already running this stage
420
756
  const wf = task.workflow;
421
757
  if (wf && wf.status === 'running' && wf.stage === task.column) {
@@ -449,12 +785,23 @@ async function waitForIdle(taskId, timeoutMs = 15000) {
449
785
  throw new Error(`waitForIdle timeout for task ${taskId}`);
450
786
  }
451
787
 
788
+ async function planDoneColumn() {
789
+ const config = await ensureBoardConfig();
790
+ return config.plan_done_column;
791
+ }
792
+
452
793
  module.exports = {
453
794
  loadConfig,
795
+ ensureBoardConfig,
796
+ clearConfigCache,
797
+ shapePublicConfig,
454
798
  maybeEnqueueOnColumnEnter,
455
799
  parseTestingVerdict,
456
800
  parseReviewGate,
457
801
  waitForIdle,
802
+ planDoneColumn,
803
+ buildTransitions,
804
+ DEFAULT_LABELS,
458
805
  JOBS_DIR,
459
806
  CONFIG_PATH
460
807
  };
package/.ai/lessons.jsonl DELETED
@@ -1,9 +0,0 @@
1
- {"id":"260729-3h4","ts":"2026-07-29","scope":"proj","tags":["tests","mcp","cli","review"],"rule":"DO Add automated tests for every new public CLI/MCP workflow before shipping","when":"api-review","sev":2,"hits":1}
2
- {"id":"260729-1ur","ts":"2026-07-29","scope":"proj","tags":["javascript","objects"],"rule":"DON'T Use empty object fallbacks in spreads like `...(obj || {})` since spreading falsy is safe","when":"object spread","sev":2,"hits":1}
3
- {"id":"260729-5eo","ts":"2026-07-29","scope":"proj","tags":["processes","mcp","ownership"],"rule":"DO Track process ownership before stopping or cleaning up discovered services","when":"gui-process management","sev":2,"hits":1}
4
- {"id":"260729-1ch","ts":"2026-07-29","scope":"proj","tags":["gui","process","mcp"],"rule":"DO GUI/MCP stop must only SIGTERM processes spawned by the current process; treat port-file PIDs as external_running","when":"gui-process management","sev":2,"hits":1}
5
- {"id":"260731-o6g","ts":"2026-07-31","scope":"proj","tags":["kanban","epics","mcp"],"rule":"DO Default list/list_epics hide done+archived; hard-delete cascades epic children; archive is reversible flag","when":"kanban cleanup context","sev":2,"hits":1}
6
- {"id":"260803-b27","ts":"2026-08-03","scope":"proj","tags":["tests","race","fs"],"rule":"DO Serialize board file mutations with an in-process lock and atomic write (temp+rename/link); race tests should assert…","when":"kanban concurrent IO","sev":2,"hits":1}
7
- {"id":"260813-9t0","ts":"2026-08-13","scope":"proj","tags":["mcp","errors","dx"],"rule":"DO MCP missing-field errors: put Valid list + Example JSON + sent keys in message so the agent can retry without guessi…","when":"MCP tool validation / agent-facing erro…","sev":2,"hits":1}
8
- {"id":"260819-ztl","ts":"2026-08-19","scope":"proj","tags":["config","kanban","validation"],"rule":"DO Field-policy config: skip only ENOENT; invalid JSON/shape must throw CONFIG_INVALID, never silently fall back to def…","when":"config load / required fields","sev":2,"hits":1}
9
- {"id":"260825-d68","ts":"2026-08-25","scope":"proj","tags":["http","adr"],"rule":"DO HTTP POST/PATCH must forward adr and evidence the same way MCP update does — doCreate already persisted evidence, on…","when":"http adr evidence","sev":2,"hits":1}
@@ -1 +0,0 @@
1
- {"goal":"push do npm","done":["session had mutations"],"next":[],"block":[],"lessons":[],"files":["/Users/mkorbas/projects/personal/markdown-kanban/mcp-server.js","/Users/mkorbas/projects/personal/markdown-kanban/kanban.js","/Users/mkorbas/projects/personal/markdown-kanban/tests/mcp-server.test.js","/Users/mkorbas/projects/personal/markdown-kanban/plan.js","/Users/mkorbas/projects/personal/markdown-kanban/CHANGELOG.md","/Users/mkorbas/projects/personal/markdown-kanban/bin/kanban.js","/Users/mkorbas/projects/personal/markdown-kanban/tests/update-tasks.test.js"],"verify":""}
@@ -1 +0,0 @@
1
- {"ts":"2026-08-25T07:24:47.875Z","sessionID":"ses_fcd476c1effeADbTA3UAQxjKMA","added":["260825-d68"],"bumped":[]}
package/.ait-quality.yml DELETED
@@ -1,25 +0,0 @@
1
- # kanbango quality policy
2
- # CLI/GUI entrypoints use console.log as user-facing output (not debug leftovers).
3
- version: 1
4
- include: []
5
- exclude:
6
- - node_modules/**
7
- - backlog/**
8
- - examples/**
9
- languages:
10
- - javascript
11
- - markdown
12
- rules:
13
- enabled: true
14
- debug-leftover: off
15
- swallowed-exception: error
16
- hidden-fallback: error
17
- unsafe-pattern: error
18
- placeholder-todo: warning
19
- dead-code: warning
20
- cognitive-complexity: warning
21
- function-length: warning
22
- nesting-depth: warning
23
- duplicate-blocks: warning
24
- gate:
25
- fail_on: error
package/bin/kanban-cmd.js DELETED
@@ -1,40 +0,0 @@
1
- #!/usr/bin/env node
2
- const { spawn } = require('child_process');
3
- const path = require('path');
4
- const os = require('os');
5
-
6
- // Find Python executable
7
- function findPython() {
8
- const pyCommands = os.platform() === 'win32' ? ['python', 'py'] : ['python3', 'python'];
9
-
10
- for (const cmd of pyCommands) {
11
- try {
12
- const result = require('child_process').spawnSync(cmd, ['--version'], { stdio: 'ignore' });
13
- if (result.status === 0) {
14
- return cmd;
15
- }
16
- } catch {
17
- continue;
18
- }
19
- }
20
- throw new Error('Python not found. Please install Python 3.7+');
21
- }
22
-
23
- // Main execution
24
- const python = findPython();
25
- const scriptPath = path.join(__dirname, '..', 'kanban-cmd.py');
26
- const args = process.argv.slice(2);
27
-
28
- const child = spawn(python, [scriptPath, ...args], {
29
- stdio: 'inherit',
30
- env: { ...process.env }
31
- });
32
-
33
- child.on('exit', (code) => {
34
- process.exit(code || 0);
35
- });
36
-
37
- child.on('error', (err) => {
38
- console.error('Error running kanban-cmd.py:', err.message);
39
- process.exit(1);
40
- });
package/kan2.md DELETED
@@ -1,76 +0,0 @@
1
- # Plan kan2: Kanbango Plan Workflow
2
-
3
- ## Cel
4
-
5
- Rozszerzyć Kanbango o niezależny workflow planów, który może być używany przez
6
- OpenCode, inne agenty i CLI. Kanbango pozostaje osobnym repozytorium.
7
-
8
- ## Zasada architektoniczna
9
-
10
- Kanbango nie zna API OpenCode ani hooków pluginu. Udostępnia stabilne operacje
11
- na planie i evidence. Plugin OpenCode jest tylko klientem oraz strażnikiem sesji.
12
-
13
- ## Funkcjonalność
14
-
15
- Dodać bibliotekę domenową oraz odpowiednie operacje CLI/MCP:
16
-
17
- - utworzenie taska z zaakceptowanym planem,
18
- - zapis kroków planu jako subtasks,
19
- - oznaczenie bieżącego kroku jako zakończonego,
20
- - zapis evidence: diff, komenda testowa, stdout, stderr i exit code,
21
- - zakończenie workflow i przeniesienie taska do `done`,
22
- - odczyt pełnego statusu planu w stabilnym formacie JSON.
23
-
24
- ## Domyślne kroki
25
-
26
- Każdy zaakceptowany plan dostaje kolejno:
27
-
28
- 1. `Write tests`
29
- 2. `Run tests and confirm red`
30
- 3. Kroki implementacyjne dostarczone przez advisora
31
- 4. `Run tests and confirm green`
32
-
33
- ## Test runner
34
-
35
- Autodetekcja musi działać per projekt, bez założenia Node:
36
-
37
- - Rust: `cargo test` przy `Cargo.toml`,
38
- - Go: `go test ./...` przy `go.mod`,
39
- - Python: `python -m pytest` przy `pyproject.toml` lub `pytest.ini`,
40
- - JavaScript/TypeScript: `npm test`, `pnpm test`, `yarn test` albo `bun test`
41
- zgodnie z lockfilem i skryptem `test` w `package.json`,
42
- - jawny override przez `OPENCODE_TEST_COMMAND`.
43
-
44
- Autodetekcja ma zwracać także powód wyboru komendy i czytelny błąd, gdy nie
45
- znaleziono testów.
46
-
47
- ## Interfejs
48
-
49
- Preferowany jest wspólny moduł JS oraz JSON CLI/MCP, zamiast parsowania tekstu:
50
-
51
- - `plan create --json <payload>`,
52
- - `plan advance --json <payload>`,
53
- - `plan evidence --json <payload>`,
54
- - `plan done --json <payload>`,
55
- - odpowiadające operacje w `kanban_manage` lub osobnym narzędziu MCP.
56
-
57
- CLI musi zwracać stabilny JSON z `ok`, `task_id`, `subtasks` i `error`.
58
-
59
- ## Testy TDD
60
-
61
- - autodetekcja runnera dla Rust, Go, Python i Node,
62
- - utworzenie planu z czterema grupami kroków,
63
- - aktualizacja kroku bez kasowania pozostałych subtasks,
64
- - zapis evidence bez silent fail,
65
- - zakończenie planu i przejście do `done`,
66
- - błędna komenda testowa i brak testów,
67
- - wywołanie przez CLI i MCP z tym samym rezultatem.
68
-
69
- ## Kolejność implementacji
70
-
71
- 1. Model planu i runner detection.
72
- 2. Operacje biblioteki Kanbango.
73
- 3. CLI i JSON output.
74
- 4. MCP adapter.
75
- 5. Integracja pluginu OpenCode z tym kontraktem.
76
- 6. Test end-to-end w przykładowym projekcie.