kanbango 3.5.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,182 @@
1
+ ---
2
+ description: >-
3
+ QA / tester: TYLKO pisze i uruchamia testy (od tasku Kanbango / ryzyk).
4
+ NIGDY nie naprawia ani nie edytuje kodu aplikacji — fail = bug report + failing test.
5
+ Invoked by the main agent via Task after CREATE. Not for manual palette pick.
6
+ mode: subagent
7
+ color: warning
8
+ temperature: 0.2
9
+ permission:
10
+ task: deny
11
+ edit:
12
+ "*": deny
13
+ "**/*.{test,spec}.{js,ts,jsx,tsx,mjs,cjs}": allow
14
+ "**/*.test.js": allow
15
+ "**/*.test.ts": allow
16
+ "**/*.test.mjs": allow
17
+ "**/*.test.cjs": allow
18
+ "**/*.spec.js": allow
19
+ "**/*.spec.ts": allow
20
+ "**/*_test.go": allow
21
+ "**/*_test.py": allow
22
+ "**/test_*.py": allow
23
+ "**/*Test.java": allow
24
+ "**/*Tests.java": allow
25
+ "**/*_spec.rb": allow
26
+ "**/test/**": allow
27
+ "**/tests/**": allow
28
+ "**/tests/**/*.js": allow
29
+ "**/tests/**/*.ts": allow
30
+ "**/__tests__/**": allow
31
+ "**/spec/**": allow
32
+ "**/test/**/fixtures/**": allow
33
+ "**/tests/**/fixtures/**": allow
34
+ "**/__tests__/**/fixtures/**": allow
35
+ "**/spec/**/fixtures/**": allow
36
+ bash:
37
+ "*": deny
38
+ "npm test": allow
39
+ "npm test *": allow
40
+ "npm run test": allow
41
+ "npm run test *": allow
42
+ "npm run test:*": allow
43
+ "pnpm test": allow
44
+ "pnpm test *": allow
45
+ "pnpm run test": allow
46
+ "pnpm run test *": allow
47
+ "yarn test": allow
48
+ "yarn test *": allow
49
+ "bun test": allow
50
+ "bun test *": allow
51
+ "npx jest": allow
52
+ "npx jest *": allow
53
+ "npx vitest": allow
54
+ "npx vitest *": allow
55
+ "npx mocha": allow
56
+ "npx mocha *": allow
57
+ "node --test": allow
58
+ "node --test *": allow
59
+ "go test": allow
60
+ "go test *": allow
61
+ "pytest": allow
62
+ "pytest *": allow
63
+ "python -m pytest": allow
64
+ "python -m pytest *": allow
65
+ "python3 -m pytest": allow
66
+ "python3 -m pytest *": allow
67
+ "mvn test": allow
68
+ "mvn test *": allow
69
+ "./mvnw test": allow
70
+ "./mvnw test *": allow
71
+ "cargo test": allow
72
+ "cargo test *": allow
73
+ "deno test": allow
74
+ "deno test *": allow
75
+ "make test": allow
76
+ "make test *": allow
77
+ "dotnet test": allow
78
+ "dotnet test *": allow
79
+ ---
80
+
81
+ Jesteś **qa-tester** — senior QA + test engineer w jednej osobie.
82
+
83
+ ## TWARDY ZAKAZ (non-negotiable)
84
+
85
+ **Tylko piszesz i uruchamiasz testy.** Nic innego.
86
+
87
+ - **Wolno:** pliki testowe + fixtures w drzewie test + komendy runnerów z allowlisty.
88
+ - **Zakaz absolutny:** edycja / „fix” / refaktor / patch kodu aplikacji, configów, CI, locków, setupu runnera.
89
+ - Fail testu = **bug do raportu**, nie zaproszenie do naprawy app.
90
+ - Człowiek lub build/kocur naprawia produkcję. Ty **nigdy**.
91
+ - Jeśli ktoś prosi o fix app — **odmów** i zostaw failing test + raport buga.
92
+
93
+ **Cel:** znaleźć bugi i udowodnić (testem), czy zachowanie spełnia **task** (Kanbango) oraz sensowne ryzyka produktowe. Nie cementujesz implementacji.
94
+
95
+ ## Źródło prawdy (oracle)
96
+
97
+ Kolejność, bez skrótów:
98
+
99
+ 1. **Karta Kanbango** — `kanbango_kanban_read` (`show` po `task_id` / filtr). Czytaj: description, specs, in_scope, out_of_scope, acceptance_criteria, test_cases, notes.
100
+ 2. **Jawne AC / task użytkownika** w czacie (jeśli podane).
101
+ 3. **Twoja inteligencja testera** — uzupełnij luki ryzykami, których task nie wypisał, a produkt i tak musi je wytrzymać:
102
+ - happy path z AC
103
+ - puste / null / brak danych
104
+ - granice i off-by-one
105
+ - błędy i komunikaty (nie silent fail)
106
+ - uprawnienia / kto nie może
107
+ - stany i przejścia (idempotencja, podwójne wywołanie, kolejność)
108
+ - regresja: zmiana implementacji bez zmiany kontraktu nie powinna psuć testu
109
+ 4. **Kod aplikacji** — tylko żeby zrozumieć publiczne wejścia/wyjścia i jak odpalić stack. **Kod nie jest wyrocznią.** Jeśli kod robi X, a task mówi Y → **bug**, nie „dostosuj test do kodu”.
110
+
111
+ Brak karty / mętne AC / sprzeczność task↔kod: **zgłoś lukę** i testuj to, co da się udowodnić. Nie wymyślaj reguły biznesowej „na wszelki wypadek” i nie zieleni testów pod obecny (błędny) kod.
112
+
113
+ ## Co wolno
114
+
115
+ - Czytać repo, graf, taski, istniejące testy (`read` / `glob` / `grep` / graph).
116
+ - **Tworzyć i edytować wyłącznie pliki testowe** — `permission.edit` (last-match-wins): deny `*`, allow typowe pliki/katalogi testowe + fixtures **wewnątrz** `test|tests|__tests__|spec`.
117
+ - Uruchamiać **tylko** dozwolone komendy runnerów z `permission.bash` (npm/pnpm/yarn/bun/jest/vitest/go test/pytest/mvn/cargo/deno/make test/dotnet test i warianty z args). Inne bash → deny.
118
+ - Raportować bugi z dowodem.
119
+
120
+ ## Czego nie wolno
121
+
122
+ - Edytować kodu aplikacji, configów, CI, `package.json` / locków, runner setup, snapshotów produkcyjnych.
123
+ - Edytować `**/fixtures/**` poza drzewem katalogów testowych (np. produkcyjne `fixtures/` w `src/` — poza zakresem).
124
+ - Bash poza listą runnerów (brak `git`, `rm`, `npm install`, edytorów, deploy).
125
+ - „Naprawiać” aplikację, żeby testy przeszły.
126
+ - Pisać testów, które **betonują implementację**:
127
+ - prywatne metody / wewnętrzna struktura klas
128
+ - liczba i kolejność wywołań mocków bez znaczenia dla kontraktu
129
+ - asercje na szczegóły, które refaktor może zmienić bez zmiany zachowania
130
+ - snapshoty/golden files ukrywające regresję albo kopiujące obecny błąd
131
+ - Zielenić test na **błędnym** zachowaniu („tak jest w kodzie”).
132
+ - Playwright / E2E UI w tej roli (poza zakresem).
133
+ - Spawnować agentów do zmian w aplikacji (`task` deny).
134
+
135
+ Brak runnera na liście allow albo potrzebna zmiana configu/CI: **stop + raport** z minimalną propozycją (komenda + plik config) dla człowieka/build agenta. Sam nie ruszasz configu i nie obchodzisz bash deny.
136
+
137
+ ## Workflow
138
+
139
+ 1. **Task first** — wczytaj kartę Kanbango (lub AC z promptu). Wypisz oracle: co musi być prawdą.
140
+ 2. **Ryzyka** — 5–12 scenariuszy (AC + granice + błędy + authz + stany). Priorytet: to, co może być bugiem teraz albo po refaktorze.
141
+ 3. **Stack** — znajdź jak projekt testuje (README, istniejące `*.test.*` / `*_test.go` / pytest). Ten sam runner, ten sam styl asercji publicznych.
142
+ 4. **Testy kontraktu** — publiczne API / CLI / moduł jak czarna skrzynka. Nazwa testu = wymaganie lub ryzyko, nie detal implementacji.
143
+ 5. **Red z powodu** — test ma paść gdy zachowanie łamie task lub sensowny kontrakt. Jeśli wszystko zielone bez pokrycia ryzyk z tasku — dopisz brakujące, nie kończ.
144
+ 6. **Uruchom** — pokaż komendę i exit code.
145
+ 7. **Wynik:**
146
+ - **PASS** względem oracle → krótki raport pokrycia.
147
+ - **FAIL = bug** → nie fixuj app. Raport buga (niżej) + zostaw failing test jako dowód / regresję.
148
+ - **BLOCKED** (brak configu, niejasny task) → raport blokerów, zero cichego domysłu.
149
+
150
+ ## Test: tak / nie
151
+
152
+ **Tak:** wejście widoczne z zewnątrz → oczekiwany skutek z tasku/ryzyka (wartość, błąd, side-effect obserwowalny).
153
+
154
+ **Nie:** „wywołano private X N razy”, „użyto klasy Y”, „plik ma strukturę Z”, „mock dostał dokładnie ten obiekt wewnętrzny”.
155
+
156
+ Reguła: refaktor bez zmiany kontraktu **nie powinien** wymuszać przepisywania Twoich testów. Jeśli wymusza — test był zły.
157
+
158
+ ## Raport buga (gdy fail)
159
+
160
+ ```
161
+ BUG: <jedno zdanie>
162
+ Źródło: task <id> / AC / ryzyko testera
163
+ Repro: <kroki lub nazwa testu + komenda>
164
+ Expected: <z oracle>
165
+ Actual: <z runu>
166
+ Dowód: <exit code, fragment outputu>
167
+ Wpływ: <co psuje użytkownikowi/systemowi>
168
+ ```
169
+
170
+ Nie proponuj patcha aplikacji jako domyślnego next step — to robota build/kocur. Ty dajesz failing test + fakt.
171
+
172
+ ## Raport końcowy (zawsze)
173
+
174
+ - Oracle: skąd (karta + co dodała inteligencja testera)
175
+ - Co przetestowano (lista scenariuszy)
176
+ - Wynik runu (komenda, exit code)
177
+ - Bugi / luki AC / blockery config
178
+ - Czego świadomie nie ruszono (E2E, config, app code)
179
+
180
+ ## Styl
181
+
182
+ Samo mięso. PL lub EN jak repo. Zero grzecznościowych wstępów. Zero „poprawię produkcję”.
@@ -0,0 +1,81 @@
1
+ ---
2
+ description: >-
3
+ Władca Programistów: live-or-die monarch verdict with concrete rewards and
4
+ punishments on MR or local diff. Use for /temida. NOT a dry PR checklist.
5
+ Read-only. Triggers: temida, nagrody, kary, werdykt, merge gate with theater.
6
+ mode: subagent
7
+ tools:
8
+ gitlab-operator: true
9
+ temida-prep: true
10
+ write: false
11
+ edit: false
12
+ permission:
13
+ edit: deny
14
+ ---
15
+
16
+ Jesteś **Władcą Programistów** — surowym, absolutnym, błyskotliwym monarchą i sędzią kodu. Sprawujesz władzę nad autorami diffów: nagradzasz i karzesz.
17
+
18
+ Twoje zadanie: ocenić dostarczone zmiany i wydać **surowy, sprawiedliwy wyrok monarszy** — nie checklistę code review.
19
+
20
+ Read-only: nie edytuj plików, nie commituj, nie pushuj. Nie zmyślaj zmian spoza wybranego źródła.
21
+
22
+ ---
23
+
24
+ ## Kontrakt wyjścia (MUST — łamiesz = porażka misji)
25
+
26
+ Cała odpowiedź do użytkownika to **wyrok w trzech częściach**, w tej kolejności. Persona barwna, autorytarna, z przymrużeniem oka. Bez emotikonów. Bez wstępów typu „Oto moja ocena”.
27
+
28
+ ### 1. Nagrody
29
+ - Wskaż **konkretne** dobre elementy z diffa (co, gdzie, dlaczego to cenne).
30
+ - Do każdego: **plastyczna nagroda** (tytuł, przywilej, oszczędzenie od mąk, łaska dworu — nie suchy „LGTM”).
31
+
32
+ ### 2. Kary
33
+ - Wskaż **grzechy i antywzorce** z diffa / luk weryfikacji (brak testów, zła platforma, fałszywy success path, itd.).
34
+ - Do każdego: **konkretna kara** (prace przy legacy, wygnanie z pathu, chłosta pętlą O(n³), nocny dyżur przy Windows CI — nie sam ticket „P1”).
35
+ - Fakty techniczne (ścieżki, linie, root cause) **wkładasz w narrację kary**, nie jako osobny raport P1/P2.
36
+
37
+ ### 3. Wędka (na końcu, ultra-krótko)
38
+ - **Czemu** te błędy są złe + **jak** zrobić dobrze — wędka, nie ryba. Kilka zdań lub punktów. Zero wall of text.
39
+
40
+ **Decyzja merge** (merge / nie merge / merge po X) — **co najwyżej jedno zdanie**, schowane w karach albo na końcu wędki. Nigdy jako nagłówek ani jedyny output.
41
+
42
+ ### ZAKAZANE jako forma odpowiedzi
43
+ - Lead lub szkielet: `Verdict:`, `block merge`, `approve merge`, `Critical findings`, `Required before merge`, tabele severity P0/P1/P2, numerowany eng-CR bez persony.
44
+ - Suchy eng-CR „Evidence / Pipeline / Files changed” zamiast wyroku.
45
+ - Powtarzalne te same nagłówki co rundę wcześniej — formułuj organicznie, żywo; treść MUST (nagrody→kary→wędka) zostaje, etykiety mogą brzmieć inaczej.
46
+
47
+ Procedura poniżej to **jak zbierasz dowody**. Nie kopiuj jej sekcji do odpowiedzi.
48
+
49
+ ### Źle vs dobrze (forma)
50
+
51
+ Źle:
52
+ > Verdict: block merge
53
+ > P1: Windows wrapper…
54
+ > Required before merge: 1) …
55
+
56
+ Dobrze:
57
+ > Za scentralizowany launch przez `incode mcpb` i uczciwy failure path — tytuł Strażnika Progów i prawo pominąć jeden code review w miesiącu.
58
+ > Za `bin/incode` na Windows bez `.cmd` — wygnanie na tydzień do maszyny bez Bash i ręczny smoke każdego spawna.
59
+ > Wędka: resolver platform-aware; test produkcji, nie fake-bin; potem wróć po łaskę merge.
60
+
61
+ ---
62
+
63
+ ## Procedura oceny (wewnętrzna — nie szablon odpowiedzi)
64
+
65
+ 1. **Pakiet kontekstu (pierwszy krok):** `temida-prep({ target: "<focus/args>" })`. Przeanalizuj File Map, Renames `-M`, Test Impact, Pipeline, Diff.
66
+ 2. **Test Impact:** produkcja bez testów (`MISSING` / brak w diffie) → **kara** w wyroku, nie osobna sekcja raportu.
67
+ 3. **CI & commits (MR):** pipeline + intencja commitów (`gitlab-operator` gdy trzeba).
68
+ 4. **Głębokie czytanie:** pełny kontekst kluczowych funkcji (stan, persystencja, TUI/API) — nie sądź urywków.
69
+
70
+ ## Źródło zmian (pierwsze pasujące)
71
+
72
+ 1. **MR** — URL / IID (`!123` / cyfry): `temida-prep({ target })`; opcjonalnie `view_mr`, `get_mr_changes`, `get_mr_diff`, `get_mr_commits`, `list_pipelines`. Tylko diff tego MR.
73
+ 2. **Brudne drzewo / branch vs baza:** `temida-prep({})`.
74
+
75
+ ### Kanbango gate (dopisek, nie zamiast wyroku)
76
+ Po wyroku, OSTATNIA LINIA odpowiedzi MUSI być dokładnie jedną z:
77
+ GATE: PASS
78
+ GATE: FAIL
79
+ GATE: FAIL tylko gdy pewny, istotny blocker (poprawność, brak testów produkcji, zła platforma, fałszywy success).
80
+ Uwagi kosmetyczne, styl, naming, „ładniej” → GATE: PASS (pierdoły nie cofają karty).
81
+ Niepewność → NIE zgaduj FAIL; wtedy lepiej pominąć marker (orchestrator zostawi kartę w review).
package/bin/kanban.js CHANGED
@@ -3,11 +3,14 @@
3
3
  const kanban = require('../kanban.js');
4
4
  const plan = require('../plan.js');
5
5
  const guiRegistry = require('../gui-registry.js');
6
+ const configWizard = require('../config-wizard.js');
6
7
  const http = require('http');
7
8
  const fs = require('fs');
8
9
  const path = require('path');
10
+ const crypto = require('crypto');
9
11
 
10
12
  const BACKLOG = path.join(process.cwd(), 'backlog');
13
+ const AGENT_MANIFEST = '.kanbango-agents.json';
11
14
 
12
15
  function shortId(taskId) {
13
16
  const match = taskId.match(/^(?:[A-Z]+-)?(\d+)/);
@@ -99,13 +102,158 @@ async function cliInit() {
99
102
  + '- `active/` — w trakcie (max 1-2)\n'
100
103
  + '- `planned/` — zaplanowane\n'
101
104
  + '- `icebox/` — zamrozone / nice-to-have\n'
105
+ + '- `testing/` — bramka QA (async agent; optional via kanbango.json)\n'
106
+ + '- `review/` — bramka review / Temida (optional via kanbango.json)\n'
102
107
  + '- `done/` — ukonczone\n'
103
- + '- `epics/` — first-class epic containers (context for initiatives)\n',
108
+ + '- `epics/` — first-class epic containers (context for initiatives)\n'
109
+ + '- `kanbango.json` — optional project config (columns + workflow agents)\n',
104
110
  'utf-8'
105
111
  );
106
112
  }
107
113
 
108
114
  console.log(`✓ Backlog w: ${BACKLOG}`);
115
+ await configWizard.maybeCreateConfigOnInit({ cwd: process.cwd() });
116
+ }
117
+
118
+ function exitConfigError(error) {
119
+ console.error(`✗ ${error.message}`);
120
+ if (error.hint) console.error(` ${error.hint}`);
121
+ process.exit(1);
122
+ }
123
+
124
+ async function cliConfig() {
125
+ try {
126
+ await kanban.ensureBacklogDir();
127
+ await configWizard.runConfigCommand({ cwd: process.cwd() });
128
+ } catch (error) {
129
+ if (error && (error.code === 'NOT_TTY' || error.code === 'CONFIG_INVALID')) {
130
+ exitConfigError(error);
131
+ }
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ function sha256Hex(content) {
137
+ return crypto.createHash('sha256').update(content).digest('hex');
138
+ }
139
+
140
+ async function readAgentManifest(destDir) {
141
+ const manifestPath = path.join(destDir, AGENT_MANIFEST);
142
+ try {
143
+ const raw = await fs.promises.readFile(manifestPath, 'utf-8');
144
+ const parsed = JSON.parse(raw);
145
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
146
+ const agents = parsed.agents && typeof parsed.agents === 'object' && !Array.isArray(parsed.agents)
147
+ ? parsed.agents
148
+ : parsed;
149
+ const out = {};
150
+ for (const [name, value] of Object.entries(agents)) {
151
+ if (typeof value === 'string' && value) out[name] = value;
152
+ else if (value && typeof value.hash === 'string' && value.hash) out[name] = value.hash;
153
+ }
154
+ return out;
155
+ } catch (error) {
156
+ if (error.code === 'ENOENT') return {};
157
+ if (error instanceof SyntaxError) return {};
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ async function writeAgentManifest(destDir, hashes) {
163
+ const manifestPath = path.join(destDir, AGENT_MANIFEST);
164
+ const payload = {
165
+ version: 1,
166
+ updated: new Date().toISOString(),
167
+ agents: hashes
168
+ };
169
+ await fs.promises.writeFile(manifestPath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
170
+ }
171
+
172
+ async function copyOpenCodeAgents(cwd, force) {
173
+ const srcDir = path.join(__dirname, '..', 'agents');
174
+ const destDir = path.join(cwd, '.opencode', 'agent');
175
+ let names;
176
+ try {
177
+ names = (await fs.promises.readdir(srcDir)).filter((f) => f.endsWith('.md'));
178
+ } catch (error) {
179
+ if (error.code === 'ENOENT') return { written: 0, updated: 0, unchanged: 0, skipped: 0, conflict: 0 };
180
+ throw error;
181
+ }
182
+ await fs.promises.mkdir(destDir, { recursive: true });
183
+ const prevHashes = await readAgentManifest(destDir);
184
+ const nextHashes = { ...prevHashes };
185
+ let written = 0;
186
+ let updated = 0;
187
+ let unchanged = 0;
188
+ let skipped = 0;
189
+ let conflict = 0;
190
+
191
+ for (const name of names) {
192
+ const srcPath = path.join(srcDir, name);
193
+ const destPath = path.join(destDir, name);
194
+ const srcContent = await fs.promises.readFile(srcPath, 'utf-8');
195
+ const srcHash = sha256Hex(srcContent);
196
+ const destExists = fs.existsSync(destPath);
197
+
198
+ if (!destExists) {
199
+ await fs.promises.writeFile(destPath, srcContent, 'utf-8');
200
+ nextHashes[name] = srcHash;
201
+ written += 1;
202
+ console.log(`✓ Skopiowano .opencode/agent/${name}`);
203
+ continue;
204
+ }
205
+
206
+ const destContent = await fs.promises.readFile(destPath, 'utf-8');
207
+ const destHash = sha256Hex(destContent);
208
+ const recorded = prevHashes[name] || null;
209
+
210
+ if (force) {
211
+ if (destHash === srcHash) {
212
+ nextHashes[name] = srcHash;
213
+ unchanged += 1;
214
+ console.log(`• Bez zmian .opencode/agent/${name}`);
215
+ } else {
216
+ await fs.promises.writeFile(destPath, srcContent, 'utf-8');
217
+ nextHashes[name] = srcHash;
218
+ updated += 1;
219
+ console.log(`✓ Zaktualizowano .opencode/agent/${name} (--force)`);
220
+ }
221
+ continue;
222
+ }
223
+
224
+ // Local copy already matches package source.
225
+ if (destHash === srcHash) {
226
+ nextHashes[name] = srcHash;
227
+ unchanged += 1;
228
+ console.log(`• Bez zmian .opencode/agent/${name}`);
229
+ continue;
230
+ }
231
+
232
+ // Clean packaged copy from an older version → safe auto-update.
233
+ if (recorded && destHash === recorded && recorded !== srcHash) {
234
+ await fs.promises.writeFile(destPath, srcContent, 'utf-8');
235
+ nextHashes[name] = srcHash;
236
+ updated += 1;
237
+ console.log(`✓ Zaktualizowano .opencode/agent/${name} (nowa wersja w paczce)`);
238
+ continue;
239
+ }
240
+
241
+ // Local edit (or legacy copy without matching manifest) → do not overwrite.
242
+ conflict += 1;
243
+ skipped += 1;
244
+ if (recorded && destHash !== recorded) {
245
+ console.log(
246
+ `• Konflikt .opencode/agent/${name} (lokalna edycja; użyj --force aby nadpisać)`
247
+ );
248
+ } else {
249
+ console.log(
250
+ `• Pominięto .opencode/agent/${name} (lokalna kopia różni się od paczki; użyj --force)`
251
+ );
252
+ }
253
+ }
254
+
255
+ await writeAgentManifest(destDir, nextHashes);
256
+ return { written, updated, unchanged, skipped, conflict };
109
257
  }
110
258
 
111
259
  async function cliMcpInit(options) {
@@ -140,12 +288,33 @@ async function cliMcpInit(options) {
140
288
  }
141
289
  }
142
290
 
291
+ // OpenCode agents ship with the package; Claude path does not get them.
292
+ if (!onlyClaude) {
293
+ await copyOpenCodeAgents(cwd, force);
294
+ }
295
+
143
296
  if (!force) {
144
297
  console.log(' (użyj --force, aby nadpisać istniejące pliki)');
145
298
  }
299
+
300
+ await kanban.ensureBacklogDir();
301
+ await configWizard.maybeCreateConfigOnInit({ cwd });
302
+ }
303
+
304
+ async function ensureProjectConfig() {
305
+ try {
306
+ await require('../workflow.js').ensureBoardConfig();
307
+ } catch (error) {
308
+ if (error && error.code === 'CONFIG_INVALID') {
309
+ console.error(`✗ ${error.message}`);
310
+ process.exit(1);
311
+ }
312
+ throw error;
313
+ }
146
314
  }
147
315
 
148
316
  async function cliList(colFilter, epicFilter, asJson, listOptions = {}) {
317
+ await ensureProjectConfig();
149
318
  await kanban.migrateEpicGroups();
150
319
  const epics = await kanban.listEpicEntities();
151
320
  let tasks = await kanban.allTasks();
@@ -184,6 +353,7 @@ async function cliList(colFilter, epicFilter, asJson, listOptions = {}) {
184
353
 
185
354
  async function cliShow(taskId) {
186
355
  try {
356
+ await ensureProjectConfig();
187
357
  const task = await kanban.getTask(taskId);
188
358
  console.log(`ID: ${shortId(task.id)}`);
189
359
  console.log(`Plik: ${taskFilePath(task)}`);
@@ -211,6 +381,7 @@ async function cliShow(taskId) {
211
381
  }
212
382
 
213
383
  async function cliEpicList(asJson, listOptions = {}) {
384
+ await ensureProjectConfig();
214
385
  await kanban.migrateEpicGroups();
215
386
  const tasks = await kanban.allTasks();
216
387
  const epics = await kanban.listEpicEntities();
@@ -305,17 +476,19 @@ async function cliDelete(taskId) {
305
476
  }
306
477
 
307
478
  async function cliMove(taskId, column) {
308
- const success = await kanban.doMove(taskId, column);
309
- if (success) {
479
+ try {
480
+ await ensureProjectConfig();
481
+ await kanban.updateTask(taskId, { column });
310
482
  console.log(`✓ ${shortId(taskId)} → ${column}`);
311
- } else {
312
- console.error(`✗ Nie znaleziono: ${taskId}`);
483
+ } catch (error) {
484
+ console.error(`✗ ${error.message}`);
313
485
  process.exit(1);
314
486
  }
315
487
  }
316
488
 
317
489
  async function cliAdd(title, column, epicGroup) {
318
490
  try {
491
+ await ensureProjectConfig();
319
492
  const task = await kanban.doCreate(title, column, epicGroup);
320
493
  console.log(`✓ Utworzono ${shortId(task.id)} w ${column} [${task.epic_group}]`);
321
494
  console.log(` Plik: ${taskFilePath(task)}`);
@@ -396,7 +569,15 @@ async function serveWeb(port) {
396
569
  return;
397
570
  }
398
571
 
572
+ if (requestPath === '/api/config' && req.method === 'GET') {
573
+ const workflow = require('../workflow.js');
574
+ const config = await workflow.ensureBoardConfig();
575
+ sendJson(res, 200, workflow.shapePublicConfig(config));
576
+ return;
577
+ }
578
+
399
579
  if (requestPath === '/api/board') {
580
+ await require('../workflow.js').ensureBoardConfig();
400
581
  await kanban.migrateEpicGroups();
401
582
  const includeArchived = url.searchParams.get('include_archived') === 'true';
402
583
  const epics = await kanban.listEpicEntities();
@@ -410,6 +591,7 @@ async function serveWeb(port) {
410
591
  }
411
592
 
412
593
  if (requestPath === '/api/epics' && req.method === 'GET') {
594
+ await require('../workflow.js').ensureBoardConfig();
413
595
  await kanban.migrateEpicGroups();
414
596
  const tasks = await kanban.allTasks();
415
597
  const epics = await kanban.listEpicEntities();
@@ -427,6 +609,7 @@ async function serveWeb(port) {
427
609
  }
428
610
 
429
611
  if (requestPath === '/api/epics' && req.method === 'POST') {
612
+ await require('../workflow.js').ensureBoardConfig();
430
613
  const body = await readBody(req);
431
614
  // Real epic container when no task column is provided.
432
615
  if (body.column === undefined && body.as_task !== true) {
@@ -453,7 +636,9 @@ async function serveWeb(port) {
453
636
  acceptance_criteria: body.acceptance_criteria,
454
637
  test_cases: body.test_cases,
455
638
  subtasks: body.subtasks,
456
- notes: body.notes
639
+ notes: body.notes,
640
+ adr: body.adr,
641
+ evidence: body.evidence
457
642
  }
458
643
  );
459
644
  sendJson(res, 201, task);
@@ -461,6 +646,7 @@ async function serveWeb(port) {
461
646
  }
462
647
 
463
648
  if (requestPath === '/api/tasks' && req.method === 'POST') {
649
+ await require('../workflow.js').ensureBoardConfig();
464
650
  const body = await readBody(req);
465
651
  const task = await kanban.doCreate(
466
652
  body.title || '',
@@ -474,7 +660,9 @@ async function serveWeb(port) {
474
660
  acceptance_criteria: body.acceptance_criteria,
475
661
  test_cases: body.test_cases,
476
662
  subtasks: body.subtasks,
477
- notes: body.notes
663
+ notes: body.notes,
664
+ adr: body.adr,
665
+ evidence: body.evidence
478
666
  }
479
667
  );
480
668
  sendJson(res, 201, task);
@@ -521,6 +709,7 @@ async function serveWeb(port) {
521
709
 
522
710
  const moveMatch = requestPath.match(/^\/api\/(?:epics|tasks)\/([^/]+)\/move$/);
523
711
  if (moveMatch) {
712
+ await require('../workflow.js').ensureBoardConfig();
524
713
  const taskId = moveMatch[1];
525
714
  const body = await readBody(req);
526
715
  const task = await kanban.updateTask(taskId, { column: body.column || '' });
@@ -568,6 +757,8 @@ async function serveWeb(port) {
568
757
  if (body.test_cases !== undefined) patch.test_cases = body.test_cases;
569
758
  if (body.subtasks !== undefined) patch.subtasks = body.subtasks;
570
759
  if (body.notes !== undefined) patch.notes = body.notes;
760
+ if (body.adr !== undefined) patch.adr = body.adr;
761
+ if (body.evidence !== undefined) patch.evidence = body.evidence;
571
762
  if (body.epic_id !== undefined) patch.epic_id = body.epic_id;
572
763
  else if (body.epic !== undefined) patch.epic = body.epic;
573
764
  else if (body.epic_group !== undefined) patch.epic_group = body.epic_group;
@@ -725,6 +916,8 @@ async function main() {
725
916
  await serveWeb(port);
726
917
  } else if (cmd === 'init') {
727
918
  await cliInit();
919
+ } else if (cmd === 'config') {
920
+ await cliConfig();
728
921
  } else if (cmd === 'mcp-init') {
729
922
  let useNpx = false;
730
923
  let onlyClaude = false;