kanbango 3.8.0 → 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/plan.js CHANGED
@@ -98,18 +98,11 @@ async function create(payload = {}) {
98
98
  }
99
99
 
100
100
  async function advance(payload = {}) {
101
- const task = await kanban.getTask(payload.task_id);
102
- const index = payload.index !== undefined ? Number(payload.index)
103
- : task.subtasks.findIndex((subtask) => !subtask.done);
104
- if (!Number.isInteger(index) || index < 0 || index >= task.subtasks.length) {
105
- throw planError('INVALID_SUBTASK_INDEX', 'No valid plan step was provided',
106
- 'Provide the zero-based index of an incomplete subtask', { index, total_subtasks: task.subtasks.length });
107
- }
108
- const subtasks = task.subtasks.map((subtask, subtaskIndex) => ({
109
- ...subtask,
110
- done: subtaskIndex === index ? true : subtask.done
111
- }));
112
- const updated = await kanban.updateTask(task.id, { subtasks });
101
+ const hasIndex = payload.index !== undefined && payload.index !== null && payload.index !== '';
102
+ const patch = hasIndex
103
+ ? { mark_subtask_done: Number(payload.index) }
104
+ : { mark_subtask_done: null };
105
+ const updated = await kanban.updateTask(payload.task_id, patch);
113
106
  return result(updated, { current_step: updated.subtasks.findIndex((subtask) => !subtask.done) });
114
107
  }
115
108
 
@@ -125,19 +118,21 @@ async function evidence(payload = {}) {
125
118
  throw planError('VALIDATION_ERROR', 'exit_code must be an integer',
126
119
  'Use the process exit code from the test command, or omit exit_code', { field: 'exit_code' });
127
120
  }
128
- const task = await kanban.getTask(payload.task_id);
129
- const patch = { evidence: [...task.evidence, {
130
- diff,
131
- test_command: testCommand,
132
- stdout: payload.stdout !== undefined ? String(payload.stdout) : '',
133
- stderr: payload.stderr !== undefined ? String(payload.stderr) : '',
134
- exit_code: Number.isInteger(payload.exit_code) ? payload.exit_code : null,
135
- summary,
136
- created: new Date().toISOString()
137
- }] };
121
+ const patch = {
122
+ appendEvidence: {
123
+ diff,
124
+ test_command: testCommand,
125
+ stdout: payload.stdout !== undefined ? String(payload.stdout) : '',
126
+ stderr: payload.stderr !== undefined ? String(payload.stderr) : '',
127
+ exit_code: Number.isInteger(payload.exit_code) ? payload.exit_code : null,
128
+ summary,
129
+ created: new Date().toISOString()
130
+ }
131
+ };
138
132
  if (payload.files !== undefined) patch.files = payload.files;
139
- const updated = await kanban.updateTask(task.id, patch);
140
- return result(updated, { evidence: patch.evidence[patch.evidence.length - 1] });
133
+ const updated = await kanban.updateTask(payload.task_id, patch);
134
+ const evidenceList = updated.evidence || [];
135
+ return result(updated, { evidence: evidenceList[evidenceList.length - 1] });
141
136
  }
142
137
 
143
138
  async function done(payload = {}) {
@@ -0,0 +1,117 @@
1
+ const path = require('path');
2
+
3
+ const LIST_COLUMNS = ['active', 'planned', 'icebox'];
4
+
5
+ const kanbanPalette = {
6
+ commandName: 'kanban.open',
7
+ title: 'Kanban',
8
+ desc: 'Browse board and paste a start prompt',
9
+ category: 'Kanban',
10
+ slashName: 'kanban',
11
+ shortcut: 'ctrl+alt+k'
12
+ };
13
+
14
+ function defaultKanban() {
15
+ return require(path.join(__dirname, '..', 'kanban.js'));
16
+ }
17
+
18
+ function formatEpicLabel(epic) {
19
+ if (!epic || !epic.id) return '—';
20
+ const title = typeof epic.title === 'string' ? epic.title.trim() : '';
21
+ return title ? `${epic.id}/${title}` : String(epic.id);
22
+ }
23
+
24
+ function formatStartPrompt(task, epic) {
25
+ const id = task && task.id != null ? String(task.id) : '';
26
+ const title = task && task.title != null ? String(task.title) : '';
27
+ return `Zacznij implementować zadanie #${id}: ${title} z epiku ${formatEpicLabel(epic)}.`;
28
+ }
29
+
30
+ function epicLookup(epics) {
31
+ const byId = new Map();
32
+ for (const epic of epics || []) {
33
+ if (epic && epic.id) byId.set(epic.id, epic);
34
+ }
35
+ return byId;
36
+ }
37
+
38
+ function shapeRow(task, epicsById, allTasks, unmetDependencies) {
39
+ const unmet = unmetDependencies(task, allTasks) || [];
40
+ const epic = task.epic_id ? epicsById.get(task.epic_id) : null;
41
+ return {
42
+ id: task.id,
43
+ title: task.title || '',
44
+ column: task.column,
45
+ epic_id: task.epic_id || '',
46
+ epicTitle: epic && epic.title ? epic.title : '',
47
+ epicLabel: formatEpicLabel(epic || (task.epic_id ? { id: task.epic_id } : null)),
48
+ blocked: unmet.length > 0,
49
+ unmet_dependencies: unmet,
50
+ description: task.description || '',
51
+ acceptance_criteria: Array.isArray(task.acceptance_criteria) ? task.acceptance_criteria : [],
52
+ depends_on: Array.isArray(task.depends_on) ? task.depends_on : []
53
+ };
54
+ }
55
+
56
+ function toBoardRows(tasks, epics, helpers = {}) {
57
+ const filterTasksForList = helpers.filterTasksForList || ((list) => list);
58
+ const unmetDependencies = helpers.unmetDependencies || (() => []);
59
+ const filtered = filterTasksForList(tasks || [], epics || [], {});
60
+ const epicsById = epicLookup(epics);
61
+ const byColumn = new Map(LIST_COLUMNS.map((column) => [column, []]));
62
+ for (const task of filtered) {
63
+ if (!task || !byColumn.has(task.column)) continue;
64
+ byColumn.get(task.column).push(shapeRow(task, epicsById, tasks, unmetDependencies));
65
+ }
66
+ return LIST_COLUMNS.flatMap((column) => byColumn.get(column));
67
+ }
68
+
69
+ async function loadBoard(options = {}) {
70
+ const kanban = options.kanban || defaultKanban();
71
+ const cwd = options.cwd;
72
+ const previous = process.cwd();
73
+ let changed = false;
74
+ try {
75
+ if (cwd && cwd !== previous) {
76
+ process.chdir(cwd);
77
+ changed = true;
78
+ }
79
+ const tasks = await kanban.allTasks();
80
+ const epics = await kanban.listEpicEntities();
81
+ return {
82
+ rows: toBoardRows(tasks, epics, {
83
+ filterTasksForList: kanban.filterTasksForList,
84
+ unmetDependencies: kanban.unmetDependencies
85
+ }),
86
+ tasks,
87
+ epics
88
+ };
89
+ } finally {
90
+ if (changed) process.chdir(previous);
91
+ }
92
+ }
93
+
94
+ function moveSelection(index, delta, length) {
95
+ if (!length || length <= 0) return 0;
96
+ const next = Number(index || 0) + Number(delta || 0);
97
+ if (next < 0) return 0;
98
+ if (next >= length) return length - 1;
99
+ return next;
100
+ }
101
+
102
+ function selectedRow(rows, index) {
103
+ if (!Array.isArray(rows) || rows.length === 0) return null;
104
+ if (index < 0 || index >= rows.length) return null;
105
+ return rows[index];
106
+ }
107
+
108
+ module.exports = {
109
+ LIST_COLUMNS,
110
+ kanbanPalette,
111
+ formatEpicLabel,
112
+ formatStartPrompt,
113
+ toBoardRows,
114
+ loadBoard,
115
+ moveSelection,
116
+ selectedRow
117
+ };
@@ -0,0 +1,314 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { createRequire } from 'node:module';
3
+ import type { TuiPlugin, TuiPluginApi, TuiPluginModule, TuiThemeCurrent } from '@opencode-ai/plugin/tui';
4
+ import { createSignal, For, Show, type Accessor } from 'solid-js';
5
+
6
+ type BoardRow = {
7
+ id: string;
8
+ title: string;
9
+ column: string;
10
+ epic_id: string;
11
+ epicTitle: string;
12
+ epicLabel: string;
13
+ blocked: boolean;
14
+ unmet_dependencies: Array<{ id: string }>;
15
+ description: string;
16
+ acceptance_criteria: string[];
17
+ depends_on: string[];
18
+ };
19
+
20
+ const requireController = createRequire(import.meta.url);
21
+ const controller = requireController('./tui-kanban-controller.js') as {
22
+ kanbanPalette: {
23
+ commandName: string;
24
+ title: string;
25
+ desc: string;
26
+ category: string;
27
+ slashName: string;
28
+ shortcut: string;
29
+ };
30
+ formatStartPrompt: (
31
+ task: { id: string; title: string },
32
+ epic: { id?: string; title?: string } | null
33
+ ) => string;
34
+ loadBoard: (options?: { cwd?: string }) => Promise<{ rows: BoardRow[] }>;
35
+ moveSelection: (index: number, delta: number, length: number) => number;
36
+ selectedRow: (rows: BoardRow[], index: number) => BoardRow | null;
37
+ };
38
+
39
+ const id = 'kanbango.tui';
40
+ type TuiApi = TuiPluginApi;
41
+ type Theme = TuiThemeCurrent;
42
+
43
+ type View =
44
+ | { status: 'loading' }
45
+ | { status: 'error'; message: string }
46
+ | { status: 'ready'; rows: BoardRow[]; selected: number };
47
+
48
+ type DialogCtrl = {
49
+ view: Accessor<View>;
50
+ reload: () => Promise<void>;
51
+ move: (delta: number) => void;
52
+ confirm: () => Promise<void>;
53
+ };
54
+
55
+ let dialogOpen = false;
56
+ let dialogCtrl: DialogCtrl | null = null;
57
+
58
+ function closeKanbanDialog(api: TuiApi) {
59
+ dialogOpen = false;
60
+ dialogCtrl = null;
61
+ api.ui.dialog.clear();
62
+ }
63
+
64
+ function createDialogController(api: TuiApi): DialogCtrl {
65
+ const [view, setView] = createSignal<View>({ status: 'loading' });
66
+
67
+ const reload = async () => {
68
+ setView({ status: 'loading' });
69
+ try {
70
+ const board = await controller.loadBoard({ cwd: api.state.path.directory });
71
+ setView({
72
+ status: 'ready',
73
+ rows: board.rows,
74
+ selected: controller.moveSelection(0, 0, board.rows.length)
75
+ });
76
+ } catch (error) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ setView({ status: 'error', message });
79
+ }
80
+ };
81
+
82
+ const move = (delta: number) => {
83
+ setView((current) => {
84
+ if (current.status !== 'ready') return current;
85
+ return {
86
+ ...current,
87
+ selected: controller.moveSelection(current.selected, delta, current.rows.length)
88
+ };
89
+ });
90
+ };
91
+
92
+ const confirm = async () => {
93
+ const current = view();
94
+ if (current.status !== 'ready') return;
95
+ const row = controller.selectedRow(current.rows, current.selected);
96
+ if (!row) return;
97
+ const epic = row.epic_id ? { id: row.epic_id, title: row.epicTitle } : null;
98
+ const text = controller.formatStartPrompt(row, epic);
99
+ await api.client.tui.appendPrompt({ text });
100
+ closeKanbanDialog(api);
101
+ };
102
+
103
+ return { view, reload, move, confirm };
104
+ }
105
+
106
+ export function openKanbanDialog(api: TuiApi) {
107
+ const ctrl = createDialogController(api);
108
+ dialogCtrl = ctrl;
109
+ dialogOpen = true;
110
+ api.ui.dialog.setSize('large');
111
+ api.ui.dialog.replace(
112
+ () => <KanbanDialog api={api} ctrl={ctrl} />,
113
+ () => {
114
+ dialogOpen = false;
115
+ if (dialogCtrl === ctrl) dialogCtrl = null;
116
+ }
117
+ );
118
+ void ctrl.reload();
119
+ }
120
+
121
+ function registerPalette(api: TuiApi) {
122
+ api.keymap.registerLayer({
123
+ commands: [
124
+ {
125
+ name: controller.kanbanPalette.commandName,
126
+ title: controller.kanbanPalette.title,
127
+ desc: controller.kanbanPalette.desc,
128
+ category: controller.kanbanPalette.category,
129
+ namespace: 'palette',
130
+ slashName: controller.kanbanPalette.slashName,
131
+ run() {
132
+ openKanbanDialog(api);
133
+ }
134
+ }
135
+ ]
136
+ });
137
+ }
138
+
139
+ function registerShortcut(api: TuiApi) {
140
+ api.keymap.registerLayer({
141
+ enabled: () => !dialogOpen,
142
+ bindings: [
143
+ {
144
+ key: controller.kanbanPalette.shortcut,
145
+ cmd: () => openKanbanDialog(api),
146
+ desc: controller.kanbanPalette.title
147
+ }
148
+ ]
149
+ });
150
+ }
151
+
152
+ function registerDialogKeys(api: TuiApi) {
153
+ api.keymap.registerLayer({
154
+ enabled: () => dialogOpen && api.ui.dialog.open,
155
+ bindings: [
156
+ { key: 'up', cmd: () => dialogCtrl?.move(-1), desc: 'Previous task' },
157
+ { key: 'down', cmd: () => dialogCtrl?.move(1), desc: 'Next task' },
158
+ { key: 'k', cmd: () => dialogCtrl?.move(-1), desc: 'Previous task' },
159
+ { key: 'j', cmd: () => dialogCtrl?.move(1), desc: 'Next task' },
160
+ { key: 'return', cmd: () => void dialogCtrl?.confirm(), desc: 'Paste start prompt' },
161
+ { key: 'escape', cmd: () => closeKanbanDialog(api), desc: 'Close' }
162
+ ]
163
+ });
164
+ }
165
+
166
+ const tui: TuiPlugin = async (api) => {
167
+ registerPalette(api);
168
+ registerShortcut(api);
169
+ registerDialogKeys(api);
170
+ };
171
+
172
+ function KanbanDialog(props: { api: TuiApi; ctrl: DialogCtrl }) {
173
+ const theme = () => props.api.theme.current as Theme;
174
+ const state = () => props.ctrl.view();
175
+ const selected = () => {
176
+ const current = state();
177
+ if (current.status !== 'ready') return null;
178
+ return controller.selectedRow(current.rows, current.selected);
179
+ };
180
+
181
+ return (
182
+ <box flexDirection="column" width="100%" height="100%" padding={1} gap={1}>
183
+ <text>
184
+ <span style={{ fg: theme().primary, bold: true }}>Kanban</span>
185
+ <span style={{ fg: theme().textMuted }}> · Enter paste start · Esc close</span>
186
+ </text>
187
+ <Show when={state().status === 'loading'}>
188
+ <text fg={theme().textMuted}>Loading board…</text>
189
+ </Show>
190
+ <Show when={state().status === 'error'}>
191
+ <text fg={theme().error}>{(state() as { message: string }).message}</text>
192
+ </Show>
193
+ <Show when={state().status === 'ready'}>
194
+ <box flexDirection="row" width="100%" height="100%" gap={1} flexGrow={1}>
195
+ <TaskList
196
+ rows={(state() as Extract<View, { status: 'ready' }>).rows}
197
+ selected={(state() as Extract<View, { status: 'ready' }>).selected}
198
+ theme={theme()}
199
+ />
200
+ <DetailPanel row={selected()} theme={theme()} />
201
+ </box>
202
+ </Show>
203
+ </box>
204
+ );
205
+ }
206
+
207
+ function groupedRows(rows: BoardRow[]) {
208
+ const groups: Array<{ column: string; items: Array<{ row: BoardRow; index: number }> }> = [];
209
+ for (let index = 0; index < rows.length; index++) {
210
+ const row = rows[index];
211
+ const last = groups[groups.length - 1];
212
+ if (!last || last.column !== row.column) {
213
+ groups.push({ column: row.column, items: [{ row, index }] });
214
+ } else {
215
+ last.items.push({ row, index });
216
+ }
217
+ }
218
+ return groups;
219
+ }
220
+
221
+ function TaskList(props: { rows: BoardRow[]; selected: number; theme: Theme }) {
222
+ const groups = () => groupedRows(props.rows);
223
+ return (
224
+ <box
225
+ flexDirection="column"
226
+ flexGrow={1}
227
+ width="50%"
228
+ height="100%"
229
+ border={true}
230
+ borderColor={props.theme.border}
231
+ padding={1}
232
+ title=" tasks "
233
+ titleAlignment="left"
234
+ >
235
+ <Show when={props.rows.length > 0} fallback={<text fg={props.theme.textMuted}>No open tasks</text>}>
236
+ <For each={groups()}>
237
+ {(group) => (
238
+ <box flexDirection="column" gap={0}>
239
+ <text>
240
+ <span style={{ fg: props.theme.accent ?? props.theme.primary, bold: true }}>
241
+ {group.column}
242
+ </span>
243
+ </text>
244
+ <For each={group.items}>
245
+ {(item) => (
246
+ <TaskRow row={item.row} selected={item.index === props.selected} theme={props.theme} />
247
+ )}
248
+ </For>
249
+ </box>
250
+ )}
251
+ </For>
252
+ </Show>
253
+ </box>
254
+ );
255
+ }
256
+
257
+ function TaskRow(props: { row: BoardRow; selected: boolean; theme: Theme }) {
258
+ const mark = props.row.blocked ? '!' : ' ';
259
+ return (
260
+ <text>
261
+ <span style={{ fg: props.theme.primary }}>{props.selected ? '❯ ' : ' '}</span>
262
+ <span style={{ fg: props.row.blocked ? props.theme.error : props.theme.textMuted }}>{mark}</span>
263
+ <span style={{ fg: props.selected ? props.theme.primary : props.theme.textMuted }}>
264
+ {` ${props.row.id} `}
265
+ </span>
266
+ <span style={{ fg: props.selected ? props.theme.text : props.theme.textMuted }}>
267
+ {props.row.title}
268
+ </span>
269
+ <span style={{ fg: props.theme.textMuted }}>{` ${props.row.epicLabel}`}</span>
270
+ </text>
271
+ );
272
+ }
273
+
274
+ function DetailPanel(props: { row: BoardRow | null; theme: Theme }) {
275
+ const row = () => props.row;
276
+ const ac = () => row()?.acceptance_criteria ?? [];
277
+ const unmet = () => (row()?.unmet_dependencies ?? []).map((item) => item.id).join(', ');
278
+ return (
279
+ <box
280
+ flexDirection="column"
281
+ flexGrow={1}
282
+ width="50%"
283
+ height="100%"
284
+ border={true}
285
+ borderColor={props.theme.borderActive ?? props.theme.primary}
286
+ padding={1}
287
+ title=" detail "
288
+ titleAlignment="left"
289
+ gap={0}
290
+ >
291
+ <Show when={row()} fallback={<text fg={props.theme.textMuted}>Select a task</text>}>
292
+ <text>
293
+ <span style={{ fg: props.theme.primary, bold: true }}>{row()!.id}</span>
294
+ <span style={{ fg: props.theme.text }}>{` ${row()!.title}`}</span>
295
+ </text>
296
+ <text fg={props.theme.textMuted}>{`epic ${row()!.epicLabel}`}</text>
297
+ <Show when={row()!.blocked}>
298
+ <text fg={props.theme.error}>{`blocked ${unmet() || 'yes'}`}</text>
299
+ </Show>
300
+ <text fg={props.theme.textMuted}>
301
+ {`depends_on ${row()!.depends_on.length ? row()!.depends_on.join(', ') : '—'}`}
302
+ </text>
303
+ <text fg={props.theme.text}>{row()!.description || '(no description)'}</text>
304
+ <text fg={props.theme.textMuted}>acceptance criteria</text>
305
+ <Show when={ac().length > 0} fallback={<text fg={props.theme.textMuted}>—</text>}>
306
+ <For each={ac()}>{(line) => <text fg={props.theme.text}>{`- ${line}`}</text>}</For>
307
+ </Show>
308
+ </Show>
309
+ </box>
310
+ );
311
+ }
312
+
313
+ const plugin: TuiPluginModule & { id: string } = { id, tui };
314
+ export default plugin;
package/workflow.js CHANGED
@@ -233,34 +233,32 @@ function planDoneTarget(testingOn, reviewOn) {
233
233
  }
234
234
 
235
235
  function defaultConfig() {
236
- const testingOn = true;
237
- const reviewOn = true;
236
+ const testingOn = false;
237
+ const reviewOn = false;
238
238
  return {
239
239
  missing: true,
240
240
  enabled: false,
241
241
  command: 'opencode',
242
- testing_agent: { enabled: true, name: 'qa-tester' },
243
- e2e_agent: { enabled: true, name: 'qa-e2e-tester' },
244
- review_agent: { enabled: true, name: 'temida' },
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
245
  timeout_ms: 600000,
246
246
  columns: {
247
247
  icebox: { enabled: true, label: DEFAULT_LABELS.icebox },
248
248
  planned: { enabled: true, label: DEFAULT_LABELS.planned },
249
249
  active: { enabled: true, label: DEFAULT_LABELS.active },
250
- testing: { enabled: true, label: DEFAULT_LABELS.testing },
251
- review: { enabled: true, label: DEFAULT_LABELS.review },
250
+ testing: { enabled: false, label: DEFAULT_LABELS.testing },
251
+ review: { enabled: false, label: DEFAULT_LABELS.review },
252
252
  done: { enabled: true, label: DEFAULT_LABELS.done }
253
253
  },
254
254
  active_cols: buildActiveCols(testingOn, reviewOn),
255
- workflow_stages: GATE_COLUMNS.slice(),
255
+ workflow_stages: [],
256
256
  transitions: buildTransitions(testingOn, reviewOn),
257
257
  plan_done_column: planDoneTarget(testingOn, reviewOn),
258
- move_order: ['icebox', 'planned', 'active', 'testing', 'review', 'done'],
258
+ move_order: ['icebox', 'planned', 'active', 'done'],
259
259
  board_columns: [
260
260
  { id: 'icebox', label: DEFAULT_LABELS.icebox },
261
261
  { id: 'planned', label: DEFAULT_LABELS.planned },
262
- { id: 'testing', label: DEFAULT_LABELS.testing },
263
- { id: 'review', label: DEFAULT_LABELS.review },
264
262
  { id: 'done', label: DEFAULT_LABELS.done }
265
263
  ]
266
264
  };
@@ -462,9 +460,12 @@ function truncate(text, max = 2000) {
462
460
 
463
461
  function parseTestingVerdict(stdout, stderr, exitCode) {
464
462
  const text = `${stdout}\n${stderr}`;
465
- if (/\bBLOCKED\b/i.test(text)) return 'blocked';
466
- if (/\bPASS\b/i.test(text) && !/\bFAIL\b/i.test(text)) return 'pass';
467
- 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';
468
469
  if (exitCode !== 0) return 'fail';
469
470
  return 'fail';
470
471
  }
package/.ai/lessons.jsonl DELETED
@@ -1,14 +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}
10
- {"id":"260826-bvz","ts":"2026-08-26","scope":"proj","tags":["gui","testing","config"],"rule":"DO Test dynamic GUI columns through runtime config, not static HTML IDs.","when":"config-driven GUI","sev":2,"hits":1}
11
- {"id":"260827-mmv","ts":"2026-08-27","scope":"proj","tags":["plan","mcp","qa"],"rule":"DO When create maps steps→subtasks, assert subtasks win if both sent; evidence accepts any of diff|summary|test_command…","when":"plan_create / MCP create steps","sev":2,"hits":1}
12
- {"id":"260827-m2q","ts":"2026-08-27","scope":"proj","tags":["plan","mcp","evidence"],"rule":"DO When create maps steps→subtasks, assert subtasks win if both sent; evidence accepts any of diff|summary|test_command","when":"plan_create / MCP create steps","sev":2,"hits":1}
13
- {"id":"260916-9im","ts":"2026-09-16","scope":"proj","tags":["kanban","mcp","context","depends-on"],"rule":"DO Agent wake: one kanban_read operation=context instead of list+show; store depends_on and files on the task so gates …","when":"kanban MCP agent context","sev":2,"hits":1}
14
- {"id":"260916-guc","ts":"2026-09-16","scope":"proj","tags":["kanban","mcp","depends-on","dag"],"rule":"DO On depends_on writes detect cycles immediately (CIRCULAR_DEPENDENCY with details.cycle); expose blocked/blocks on li…","when":"kanban DAG depends_on","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-09-16T10:23:54.497Z","sessionID":"ses_f5682fb36ffeOu9sf2lXfIUCGB","added":["260916-guc"],"bumped":[]}
package/.ait-quality.yml DELETED
@@ -1,26 +0,0 @@
1
- # kanbango quality policy
2
- # CLI/GUI entrypoints use console.log as user-facing output (not debug leftovers).
3
- # This file is local policy only — kanbango does not depend on or invoke ait-quality.
4
- version: 1
5
- include: []
6
- exclude:
7
- - node_modules/**
8
- - backlog/**
9
- - examples/**
10
- languages:
11
- - javascript
12
- - markdown
13
- rules:
14
- enabled: true
15
- debug-leftover: off
16
- swallowed-exception: error
17
- hidden-fallback: error
18
- unsafe-pattern: error
19
- placeholder-todo: warning
20
- dead-code: warning
21
- cognitive-complexity: warning
22
- function-length: warning
23
- nesting-depth: warning
24
- duplicate-blocks: warning
25
- gate:
26
- 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
- });