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.
- package/.ai/lessons.jsonl +6 -0
- package/.ai/retro/last-run.json +1 -1
- package/.ait-quality.yml +1 -0
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +77 -0
- package/LLM_AGENTS.md +1 -1
- package/README.md +34 -3
- package/agent-playbook.js +31 -7
- package/agents/qa-e2e-tester.md +315 -0
- package/agents/qa-tester.md +182 -0
- package/agents/temida.md +81 -0
- package/bin/kanban.js +200 -7
- package/config-wizard.js +441 -0
- package/index.html +115 -23
- package/index.js +2 -0
- package/kanban.js +578 -33
- package/mcp-server.js +89 -21
- package/package.json +1 -1
- package/plan.js +34 -27
- package/tests/fixtures/fake-opencode.js +69 -0
- package/tests/index.js +19 -0
- package/tests/kanban-cli.js +118 -0
- package/tests/kanban.js +104 -0
- package/tests/run.js +9 -0
- package/workflow.js +806 -0
|
@@ -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ę”.
|
package/agents/temida.md
ADDED
|
@@ -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
|
-
|
|
309
|
-
|
|
479
|
+
try {
|
|
480
|
+
await ensureProjectConfig();
|
|
481
|
+
await kanban.updateTask(taskId, { column });
|
|
310
482
|
console.log(`✓ ${shortId(taskId)} → ${column}`);
|
|
311
|
-
}
|
|
312
|
-
console.error(`✗
|
|
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;
|