kanbango 2.1.0 → 2.4.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/AGENTS.md +7 -7
- package/API.md +20 -12
- package/CHANGELOG.md +32 -0
- package/LLM_AGENTS.md +141 -175
- package/README.md +31 -14
- package/bin/kanban.js +101 -26
- package/index.html +162 -85
- package/index.js +2 -0
- package/kan2.md +76 -0
- package/kanban.js +263 -34
- package/mcp-server.js +389 -185
- package/package.json +1 -1
- package/plan.js +159 -0
- package/planv2.md +17 -7
- package/tests/run.js +1 -0
package/index.html
CHANGED
|
@@ -71,6 +71,15 @@ header h1 { font-size: 14px; font-weight: 700; letter-spacing: -0.2px; }
|
|
|
71
71
|
flex-shrink: 0;
|
|
72
72
|
}
|
|
73
73
|
.new-epic-strip.open { display: flex; }
|
|
74
|
+
.ne-field { display: flex; flex-direction: column; gap: 3px; }
|
|
75
|
+
.ne-label {
|
|
76
|
+
font-size: 10px;
|
|
77
|
+
font-weight: 700;
|
|
78
|
+
text-transform: uppercase;
|
|
79
|
+
letter-spacing: 0.6px;
|
|
80
|
+
color: var(--muted);
|
|
81
|
+
}
|
|
82
|
+
.ne-buttons { display: flex; gap: 8px; align-items: flex-end; }
|
|
74
83
|
.ne-input {
|
|
75
84
|
border: 1px solid var(--border);
|
|
76
85
|
border-radius: var(--radius);
|
|
@@ -378,6 +387,20 @@ header h1 { font-size: 14px; font-weight: 700; letter-spacing: -0.2px; }
|
|
|
378
387
|
line-height: 1.45;
|
|
379
388
|
color: var(--text);
|
|
380
389
|
}
|
|
390
|
+
.detail-section.out-of-scope {
|
|
391
|
+
border-left: 3px solid #e8b4b4;
|
|
392
|
+
padding-left: 8px;
|
|
393
|
+
}
|
|
394
|
+
.detail-section.out-of-scope .detail-label {
|
|
395
|
+
color: #a86a6a;
|
|
396
|
+
}
|
|
397
|
+
.detail-section.out-of-scope .detail-list {
|
|
398
|
+
color: #7a5a5a;
|
|
399
|
+
}
|
|
400
|
+
.edit-block.out-of-scope {
|
|
401
|
+
border-color: #e8c4c4;
|
|
402
|
+
background: #fffbfb;
|
|
403
|
+
}
|
|
381
404
|
.edit-meta {
|
|
382
405
|
display: flex;
|
|
383
406
|
flex-direction: column;
|
|
@@ -396,6 +419,13 @@ header h1 { font-size: 14px; font-weight: 700; letter-spacing: -0.2px; }
|
|
|
396
419
|
flex-direction: column;
|
|
397
420
|
gap: 6px;
|
|
398
421
|
}
|
|
422
|
+
.lane-add-label {
|
|
423
|
+
font-size: 10px;
|
|
424
|
+
font-weight: 700;
|
|
425
|
+
text-transform: uppercase;
|
|
426
|
+
letter-spacing: 0.6px;
|
|
427
|
+
color: var(--muted);
|
|
428
|
+
}
|
|
399
429
|
.lane-add-title {
|
|
400
430
|
width: 100%;
|
|
401
431
|
border: 1px solid var(--planned);
|
|
@@ -457,20 +487,31 @@ header h1 { font-size: 14px; font-weight: 700; letter-spacing: -0.2px; }
|
|
|
457
487
|
<h1>Donna Kanban</h1>
|
|
458
488
|
<span class="hstats" id="hstats">—</span>
|
|
459
489
|
<span class="hspacer"></span>
|
|
460
|
-
<button class="btn-new" id="btn-new" onclick="toggleNewForm()">+
|
|
490
|
+
<button class="btn-new" id="btn-new" onclick="toggleNewForm()">+ New task</button>
|
|
461
491
|
</header>
|
|
462
492
|
|
|
463
493
|
<div class="new-epic-strip" id="new-strip">
|
|
464
|
-
<
|
|
465
|
-
|
|
466
|
-
<
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
<
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
494
|
+
<div class="ne-field">
|
|
495
|
+
<div class="ne-label">Title</div>
|
|
496
|
+
<input class="ne-input ne-title" id="ne-title" placeholder="Task name…" autocomplete="off">
|
|
497
|
+
</div>
|
|
498
|
+
<div class="ne-field">
|
|
499
|
+
<div class="ne-label">Column</div>
|
|
500
|
+
<select class="ne-input ne-col" id="ne-col">
|
|
501
|
+
<option value="icebox">🧊 Icebox</option>
|
|
502
|
+
<option value="planned" selected>📦 Planned</option>
|
|
503
|
+
<option value="active">🔥 Active</option>
|
|
504
|
+
<option value="done">✅ Done</option>
|
|
505
|
+
</select>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="ne-field">
|
|
508
|
+
<div class="ne-label">Epic</div>
|
|
509
|
+
<input class="ne-input ne-group" id="ne-group" placeholder="Epic group (optional)…" autocomplete="off">
|
|
510
|
+
</div>
|
|
511
|
+
<div class="ne-buttons">
|
|
512
|
+
<button class="btn-sm btn-confirm" onclick="submitNew()">Add</button>
|
|
513
|
+
<button class="btn-sm btn-cancel" onclick="toggleNewForm()">Cancel</button>
|
|
514
|
+
</div>
|
|
474
515
|
</div>
|
|
475
516
|
|
|
476
517
|
<div id="board-wrap">
|
|
@@ -511,7 +552,7 @@ function render() {
|
|
|
511
552
|
// Header stats
|
|
512
553
|
const total = allEpics.length;
|
|
513
554
|
const done = allEpics.filter(e => e.column === "done").length;
|
|
514
|
-
document.getElementById("hstats").textContent = `${done}/${total}
|
|
555
|
+
document.getElementById("hstats").textContent = `${done}/${total} done`;
|
|
515
556
|
|
|
516
557
|
// Col headers
|
|
517
558
|
const hdr = document.getElementById("col-headers");
|
|
@@ -551,15 +592,15 @@ function renderSwimlane({ name, epics }) {
|
|
|
551
592
|
|
|
552
593
|
// Header
|
|
553
594
|
const hdr = el("div", `sl-header${isCollapsed ? " collapsed" : ""}`);
|
|
554
|
-
const totalTasks = epics.reduce((s, e) => s + e.
|
|
555
|
-
const doneTasks = epics.reduce((s, e) => s + e.
|
|
595
|
+
const totalTasks = epics.reduce((s, e) => s + e.subtasks.length, 0);
|
|
596
|
+
const doneTasks = epics.reduce((s, e) => s + e.subtasks.filter(t => t.done).length, 0);
|
|
556
597
|
const pct = totalTasks > 0 ? Math.round(doneTasks / totalTasks * 100) : null;
|
|
557
598
|
|
|
558
599
|
hdr.innerHTML = `
|
|
559
600
|
<span class="sl-arrow">▼</span>
|
|
560
|
-
<span class="sl-name">${escHtml(name === "—" ? "
|
|
601
|
+
<span class="sl-name">${escHtml(name === "—" ? "Uncategorized (no epic)" : name)}</span>
|
|
561
602
|
<span class="sl-cnt">${epics.length}</span>
|
|
562
|
-
${pct !== null ? `<span class="sl-progress" style="margin-left:auto">${pct}%
|
|
603
|
+
${pct !== null ? `<span class="sl-progress" style="margin-left:auto">${pct}% tasks done</span>` : ""}
|
|
563
604
|
`;
|
|
564
605
|
hdr.onclick = () => {
|
|
565
606
|
if (collapsed.has(name)) collapsed.delete(name);
|
|
@@ -577,7 +618,7 @@ function renderSwimlane({ name, epics }) {
|
|
|
577
618
|
if (col.id !== "done") {
|
|
578
619
|
const addBtn = document.createElement("button");
|
|
579
620
|
addBtn.className = "lane-add";
|
|
580
|
-
addBtn.textContent = "+
|
|
621
|
+
addBtn.textContent = "+ Add here";
|
|
581
622
|
addBtn.onclick = e => { e.stopPropagation(); showLaneAdd(lane, col.id, name, addBtn); };
|
|
582
623
|
lane.appendChild(addBtn);
|
|
583
624
|
}
|
|
@@ -613,8 +654,8 @@ function renderCard(epic, colId) {
|
|
|
613
654
|
const card = el("div", `card col-${colId}${isExpanded ? " open" : ""}${isEditing ? " editing" : ""}`);
|
|
614
655
|
card.dataset.id = epic.id;
|
|
615
656
|
|
|
616
|
-
const totalT = epic.
|
|
617
|
-
const doneT = epic.
|
|
657
|
+
const totalT = epic.subtasks.length;
|
|
658
|
+
const doneT = epic.subtasks.filter(t => t.done).length;
|
|
618
659
|
const pct = totalT > 0 ? Math.round(doneT / totalT * 100) : 0;
|
|
619
660
|
const shortId = (epic.id.match(/^PI-\d+/) || [epic.id])[0];
|
|
620
661
|
const displayTitle = epic.title.replace(/^[\w.-]+:\s*/, "");
|
|
@@ -646,7 +687,7 @@ function renderCard(epic, colId) {
|
|
|
646
687
|
// For icebox show just a count badge
|
|
647
688
|
const badge = el("span", "prog-txt");
|
|
648
689
|
badge.style.cssText = "display:inline-block;margin-top:5px;background:rgba(100,116,139,0.12);border-radius:4px;padding:1px 6px;";
|
|
649
|
-
badge.textContent = `${totalT}
|
|
690
|
+
badge.textContent = `${totalT} subtasks`;
|
|
650
691
|
card.appendChild(badge);
|
|
651
692
|
}
|
|
652
693
|
|
|
@@ -673,57 +714,57 @@ function renderCard(epic, colId) {
|
|
|
673
714
|
actions.appendChild(rb);
|
|
674
715
|
}
|
|
675
716
|
|
|
676
|
-
// Expand
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
actions.appendChild(expBtn);
|
|
683
|
-
}
|
|
717
|
+
// Expand details button
|
|
718
|
+
const expBtn = document.createElement("button");
|
|
719
|
+
expBtn.className = "cbtn";
|
|
720
|
+
expBtn.textContent = isExpanded ? "▲" : "▼ Details";
|
|
721
|
+
expBtn.onclick = e => { e.stopPropagation(); toggleExpand(epic.id); };
|
|
722
|
+
actions.appendChild(expBtn);
|
|
684
723
|
|
|
685
724
|
// Edit button
|
|
686
725
|
const editBtn = document.createElement("button");
|
|
687
726
|
editBtn.className = "cbtn edit-btn";
|
|
688
727
|
editBtn.textContent = "✎";
|
|
689
|
-
editBtn.title = "
|
|
728
|
+
editBtn.title = "Edit";
|
|
690
729
|
editBtn.onclick = e => { e.stopPropagation(); startEdit(epic.id); };
|
|
691
730
|
actions.appendChild(editBtn);
|
|
692
731
|
|
|
693
732
|
card.appendChild(actions);
|
|
694
733
|
|
|
695
|
-
//
|
|
696
|
-
if (isExpanded
|
|
697
|
-
const lbl = el("div", "subtask-label");
|
|
698
|
-
lbl.textContent = "Subtaski";
|
|
699
|
-
card.appendChild(lbl);
|
|
700
|
-
|
|
734
|
+
// Details + subtasks (expanded, view mode)
|
|
735
|
+
if (isExpanded) {
|
|
701
736
|
appendDetailSections(card, epic);
|
|
702
737
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
738
|
+
if (totalT > 0) {
|
|
739
|
+
const lbl = el("div", "subtask-label");
|
|
740
|
+
lbl.textContent = "Subtasks";
|
|
741
|
+
card.appendChild(lbl);
|
|
742
|
+
|
|
743
|
+
const list = el("div", "tasks-list");
|
|
744
|
+
epic.subtasks.forEach((task, idx) => {
|
|
745
|
+
const row = el("div", `task-row${task.done ? " done" : ""}`);
|
|
746
|
+
const cbId = `v-${epic.id}-${idx}`;
|
|
747
|
+
row.innerHTML = `
|
|
748
|
+
<input type="checkbox" id="${cbId}" ${task.done ? "checked" : ""}>
|
|
749
|
+
<div class="task-copy">
|
|
750
|
+
<label for="${cbId}">${escHtml(task.text)}</label>
|
|
751
|
+
${task.description ? `<div class="task-desc">${escHtml(task.description)}</div>` : ""}
|
|
752
|
+
</div>
|
|
753
|
+
`;
|
|
754
|
+
row.querySelector("input").onchange = async function() {
|
|
755
|
+
this.disabled = true;
|
|
756
|
+
await fetch(`/api/epics/${enc(epic.id)}/tasks/${idx}`, { method: "PATCH" });
|
|
757
|
+
await load();
|
|
758
|
+
};
|
|
759
|
+
list.appendChild(row);
|
|
760
|
+
});
|
|
761
|
+
card.appendChild(list);
|
|
762
|
+
}
|
|
722
763
|
}
|
|
723
764
|
|
|
724
765
|
card.onclick = e => {
|
|
725
766
|
if (e.target.tagName === "BUTTON" || e.target.tagName === "INPUT" || e.target.tagName === "LABEL") return;
|
|
726
|
-
|
|
767
|
+
toggleExpand(epic.id);
|
|
727
768
|
};
|
|
728
769
|
}
|
|
729
770
|
|
|
@@ -736,9 +777,12 @@ function renderEditMode(card, epic, colId, shortId) {
|
|
|
736
777
|
title: displayTitle,
|
|
737
778
|
description: epic.description || "",
|
|
738
779
|
specs: epic.specs || "",
|
|
780
|
+
in_scope: (epic.in_scope || []).join("\n"),
|
|
781
|
+
out_of_scope: (epic.out_of_scope || []).join("\n"),
|
|
739
782
|
acceptance_criteria: (epic.acceptance_criteria || []).join("\n"),
|
|
783
|
+
test_cases: (epic.test_cases || []).join("\n"),
|
|
740
784
|
notes: epic.notes || "",
|
|
741
|
-
tasks: epic.
|
|
785
|
+
tasks: epic.subtasks.map(t => ({ ...t }))
|
|
742
786
|
};
|
|
743
787
|
dirtyEdits[epic.id] = dirty;
|
|
744
788
|
|
|
@@ -756,28 +800,40 @@ function renderEditMode(card, epic, colId, shortId) {
|
|
|
756
800
|
card.appendChild(titleInput);
|
|
757
801
|
|
|
758
802
|
const meta = el("div", "edit-meta");
|
|
759
|
-
meta.appendChild(renderEditBlock("
|
|
803
|
+
meta.appendChild(renderEditBlock("Description", dirty.description, value => {
|
|
760
804
|
dirty.description = value;
|
|
761
805
|
dirtyEdits[epic.id] = dirty;
|
|
762
|
-
}, "
|
|
806
|
+
}, "Context and implementation plan..."));
|
|
763
807
|
meta.appendChild(renderEditBlock("Specs", dirty.specs, value => {
|
|
764
808
|
dirty.specs = value;
|
|
765
809
|
dirtyEdits[epic.id] = dirty;
|
|
766
|
-
}, "
|
|
810
|
+
}, "APIs, constraints, edge cases..."));
|
|
811
|
+
meta.appendChild(renderEditBlock("In Scope", dirty.in_scope, value => {
|
|
812
|
+
dirty.in_scope = value;
|
|
813
|
+
dirtyEdits[epic.id] = dirty;
|
|
814
|
+
}, "What this task covers (one item per line)"));
|
|
815
|
+
meta.appendChild(renderEditBlock("Out of Scope", dirty.out_of_scope, value => {
|
|
816
|
+
dirty.out_of_scope = value;
|
|
817
|
+
dirtyEdits[epic.id] = dirty;
|
|
818
|
+
}, "What is explicitly not included (one item per line)", "out-of-scope"));
|
|
767
819
|
meta.appendChild(renderEditBlock("Acceptance Criteria", dirty.acceptance_criteria, value => {
|
|
768
820
|
dirty.acceptance_criteria = value;
|
|
769
821
|
dirtyEdits[epic.id] = dirty;
|
|
770
|
-
}, "
|
|
822
|
+
}, "One condition per line"));
|
|
823
|
+
meta.appendChild(renderEditBlock("Test Cases", dirty.test_cases, value => {
|
|
824
|
+
dirty.test_cases = value;
|
|
825
|
+
dirtyEdits[epic.id] = dirty;
|
|
826
|
+
}, "One scenario per line"));
|
|
771
827
|
meta.appendChild(renderEditBlock("Notes", dirty.notes, value => {
|
|
772
828
|
dirty.notes = value;
|
|
773
829
|
dirtyEdits[epic.id] = dirty;
|
|
774
|
-
}, "
|
|
830
|
+
}, "Optional notes"));
|
|
775
831
|
card.appendChild(meta);
|
|
776
832
|
|
|
777
833
|
// Subtasks section
|
|
778
834
|
const editTasks = el("div", "edit-tasks");
|
|
779
835
|
const subtaskLbl = el("div", "subtask-label");
|
|
780
|
-
subtaskLbl.textContent = "
|
|
836
|
+
subtaskLbl.textContent = "Subtasks";
|
|
781
837
|
editTasks.appendChild(subtaskLbl);
|
|
782
838
|
|
|
783
839
|
const taskRows = el("div");
|
|
@@ -808,14 +864,14 @@ function renderEditMode(card, epic, colId, shortId) {
|
|
|
808
864
|
|
|
809
865
|
const desc = document.createElement("textarea");
|
|
810
866
|
desc.className = "edit-task-desc";
|
|
811
|
-
desc.placeholder = "
|
|
867
|
+
desc.placeholder = "Subtask description...";
|
|
812
868
|
desc.value = task.description || "";
|
|
813
869
|
desc.oninput = () => { dirty.tasks[idx].description = desc.value; dirtyEdits[epic.id] = dirty; };
|
|
814
870
|
|
|
815
871
|
const delBtn = document.createElement("button");
|
|
816
872
|
delBtn.className = "del-task";
|
|
817
873
|
delBtn.textContent = "×";
|
|
818
|
-
delBtn.title = "
|
|
874
|
+
delBtn.title = "Remove subtask";
|
|
819
875
|
delBtn.onclick = () => { dirty.tasks.splice(idx, 1); dirtyEdits[epic.id] = dirty; refreshRows(); };
|
|
820
876
|
|
|
821
877
|
fields.appendChild(inp);
|
|
@@ -830,7 +886,7 @@ function renderEditMode(card, epic, colId, shortId) {
|
|
|
830
886
|
// Add subtask input
|
|
831
887
|
const addSubtaskInput = document.createElement("input");
|
|
832
888
|
addSubtaskInput.className = "add-task-inp";
|
|
833
|
-
addSubtaskInput.placeholder = "+
|
|
889
|
+
addSubtaskInput.placeholder = "+ New subtask… (Enter)";
|
|
834
890
|
addSubtaskInput.onkeydown = e => {
|
|
835
891
|
if (e.key === "Enter" && addSubtaskInput.value.trim()) {
|
|
836
892
|
dirty.tasks.push({ done: false, text: addSubtaskInput.value.trim(), description: "" });
|
|
@@ -846,12 +902,12 @@ function renderEditMode(card, epic, colId, shortId) {
|
|
|
846
902
|
const footer = el("div", "edit-footer");
|
|
847
903
|
const saveBtn = document.createElement("button");
|
|
848
904
|
saveBtn.className = "btn-sm btn-confirm";
|
|
849
|
-
saveBtn.textContent = "✓
|
|
905
|
+
saveBtn.textContent = "✓ Save";
|
|
850
906
|
saveBtn.onclick = async e => { e.stopPropagation(); await saveEdit(epic.id); };
|
|
851
907
|
|
|
852
|
-
|
|
908
|
+
const cancelBtn = document.createElement("button");
|
|
853
909
|
cancelBtn.className = "btn-sm btn-cancel";
|
|
854
|
-
cancelBtn.textContent = "
|
|
910
|
+
cancelBtn.textContent = "Cancel";
|
|
855
911
|
cancelBtn.onclick = e => {
|
|
856
912
|
e.stopPropagation(); delete dirtyEdits[epic.id]; editingId = null; render();
|
|
857
913
|
};
|
|
@@ -881,9 +937,12 @@ async function saveEdit(epicId) {
|
|
|
881
937
|
if (dirty.title !== origTitle) body.title = dirty.title;
|
|
882
938
|
body.description = dirty.description;
|
|
883
939
|
body.specs = dirty.specs;
|
|
940
|
+
body.in_scope = splitLines(dirty.in_scope);
|
|
941
|
+
body.out_of_scope = splitLines(dirty.out_of_scope);
|
|
884
942
|
body.acceptance_criteria = splitLines(dirty.acceptance_criteria);
|
|
943
|
+
body.test_cases = splitLines(dirty.test_cases);
|
|
885
944
|
body.notes = dirty.notes;
|
|
886
|
-
body.
|
|
945
|
+
body.subtasks = dirty.tasks.filter(t => t.text.trim()).map((task, idx) => ({
|
|
887
946
|
id: task.id || `st-${idx + 1}`,
|
|
888
947
|
text: task.text.trim(),
|
|
889
948
|
done: Boolean(task.done),
|
|
@@ -897,8 +956,8 @@ async function saveEdit(epicId) {
|
|
|
897
956
|
body: JSON.stringify(body),
|
|
898
957
|
});
|
|
899
958
|
if (!res.ok) throw new Error();
|
|
900
|
-
toast("
|
|
901
|
-
} catch { toast("
|
|
959
|
+
toast("Saved ✓");
|
|
960
|
+
} catch { toast("Save error!", true); }
|
|
902
961
|
delete dirtyEdits[epicId];
|
|
903
962
|
editingId = null;
|
|
904
963
|
await load();
|
|
@@ -917,22 +976,31 @@ function showLaneAdd(lane, colId, groupName, addBtn) {
|
|
|
917
976
|
if (existing) { existing.remove(); return; }
|
|
918
977
|
|
|
919
978
|
const wrap = el("div", "lane-addinp");
|
|
979
|
+
|
|
980
|
+
const titleLabel = el("div", "lane-add-label");
|
|
981
|
+
titleLabel.textContent = "Task title";
|
|
982
|
+
wrap.appendChild(titleLabel);
|
|
983
|
+
|
|
920
984
|
const inp = document.createElement("input");
|
|
921
985
|
inp.className = "lane-add-title";
|
|
922
|
-
inp.placeholder = "
|
|
986
|
+
inp.placeholder = "New task name…";
|
|
923
987
|
inp.autocomplete = "off";
|
|
924
988
|
|
|
989
|
+
const descLabel = el("div", "lane-add-label");
|
|
990
|
+
descLabel.textContent = "Description";
|
|
991
|
+
wrap.appendChild(descLabel);
|
|
992
|
+
|
|
925
993
|
const description = document.createElement("textarea");
|
|
926
994
|
description.className = "lane-add-textarea";
|
|
927
|
-
description.placeholder = "
|
|
995
|
+
description.placeholder = "Task description (optional)...";
|
|
928
996
|
|
|
929
997
|
const actions = el("div", "lane-add-actions");
|
|
930
998
|
const confirmBtn = document.createElement("button");
|
|
931
999
|
confirmBtn.className = "btn-sm btn-confirm";
|
|
932
|
-
confirmBtn.textContent = "
|
|
1000
|
+
confirmBtn.textContent = "Add";
|
|
933
1001
|
const cancelBtn = document.createElement("button");
|
|
934
1002
|
cancelBtn.className = "btn-sm btn-cancel";
|
|
935
|
-
cancelBtn.textContent = "
|
|
1003
|
+
cancelBtn.textContent = "Cancel";
|
|
936
1004
|
|
|
937
1005
|
const doAdd = async () => {
|
|
938
1006
|
const title = inp.value.trim();
|
|
@@ -950,11 +1018,11 @@ function showLaneAdd(lane, colId, groupName, addBtn) {
|
|
|
950
1018
|
}),
|
|
951
1019
|
});
|
|
952
1020
|
if (res.ok) {
|
|
953
|
-
toast("
|
|
1021
|
+
toast("Added ✓");
|
|
954
1022
|
wrap.remove();
|
|
955
1023
|
await load();
|
|
956
1024
|
} else {
|
|
957
|
-
toast("
|
|
1025
|
+
toast("Error!", true);
|
|
958
1026
|
inp.disabled = false;
|
|
959
1027
|
description.disabled = false;
|
|
960
1028
|
inp.focus();
|
|
@@ -996,13 +1064,13 @@ async function submitNew() {
|
|
|
996
1064
|
body: JSON.stringify({ title, column: col, epic_group: group || "—" }),
|
|
997
1065
|
});
|
|
998
1066
|
if (res.ok) {
|
|
999
|
-
toast("
|
|
1067
|
+
toast("Task added ✓");
|
|
1000
1068
|
document.getElementById("ne-title").value = "";
|
|
1001
1069
|
document.getElementById("ne-group").value = "";
|
|
1002
1070
|
toggleNewForm();
|
|
1003
1071
|
await load();
|
|
1004
1072
|
} else {
|
|
1005
|
-
toast("
|
|
1073
|
+
toast("Error!", true);
|
|
1006
1074
|
}
|
|
1007
1075
|
}
|
|
1008
1076
|
|
|
@@ -1024,14 +1092,23 @@ function splitLines(value) {
|
|
|
1024
1092
|
}
|
|
1025
1093
|
function appendDetailSections(card, epic) {
|
|
1026
1094
|
if (epic.description) {
|
|
1027
|
-
card.appendChild(renderDetailSection("
|
|
1095
|
+
card.appendChild(renderDetailSection("Description", epic.description));
|
|
1028
1096
|
}
|
|
1029
1097
|
if (epic.specs) {
|
|
1030
1098
|
card.appendChild(renderDetailSection("Specs", epic.specs));
|
|
1031
1099
|
}
|
|
1100
|
+
if (epic.in_scope && epic.in_scope.length > 0) {
|
|
1101
|
+
card.appendChild(renderDetailListSection("In Scope", epic.in_scope));
|
|
1102
|
+
}
|
|
1103
|
+
if (epic.out_of_scope && epic.out_of_scope.length > 0) {
|
|
1104
|
+
card.appendChild(renderDetailListSection("Out of Scope", epic.out_of_scope, "out-of-scope"));
|
|
1105
|
+
}
|
|
1032
1106
|
if (epic.acceptance_criteria && epic.acceptance_criteria.length > 0) {
|
|
1033
1107
|
card.appendChild(renderDetailListSection("Acceptance Criteria", epic.acceptance_criteria));
|
|
1034
1108
|
}
|
|
1109
|
+
if (epic.test_cases && epic.test_cases.length > 0) {
|
|
1110
|
+
card.appendChild(renderDetailListSection("Test Cases", epic.test_cases));
|
|
1111
|
+
}
|
|
1035
1112
|
if (epic.notes) {
|
|
1036
1113
|
card.appendChild(renderDetailSection("Notes", epic.notes));
|
|
1037
1114
|
}
|
|
@@ -1046,8 +1123,8 @@ function renderDetailSection(label, text) {
|
|
|
1046
1123
|
wrap.appendChild(textEl);
|
|
1047
1124
|
return wrap;
|
|
1048
1125
|
}
|
|
1049
|
-
function renderDetailListSection(label, items) {
|
|
1050
|
-
const wrap = el("div", "detail-section");
|
|
1126
|
+
function renderDetailListSection(label, items, extraClass) {
|
|
1127
|
+
const wrap = el("div", extraClass ? `detail-section ${extraClass}` : "detail-section");
|
|
1051
1128
|
const labelEl = el("div", "detail-label");
|
|
1052
1129
|
labelEl.textContent = label;
|
|
1053
1130
|
const list = el("ul", "detail-list");
|
|
@@ -1060,12 +1137,12 @@ function renderDetailListSection(label, items) {
|
|
|
1060
1137
|
wrap.appendChild(list);
|
|
1061
1138
|
return wrap;
|
|
1062
1139
|
}
|
|
1063
|
-
function renderEditBlock(label, value, onInput, placeholder) {
|
|
1064
|
-
const wrap = el("div");
|
|
1140
|
+
function renderEditBlock(label, value, onInput, placeholder, extraClass) {
|
|
1141
|
+
const wrap = el("div", extraClass ? `detail-section ${extraClass}` : null);
|
|
1065
1142
|
const labelEl = el("div", "detail-label");
|
|
1066
1143
|
labelEl.textContent = label;
|
|
1067
1144
|
const textarea = document.createElement("textarea");
|
|
1068
|
-
textarea.className = "edit-block";
|
|
1145
|
+
textarea.className = extraClass ? `edit-block ${extraClass}` : "edit-block";
|
|
1069
1146
|
textarea.value = value;
|
|
1070
1147
|
textarea.placeholder = placeholder;
|
|
1071
1148
|
textarea.oninput = () => onInput(textarea.value);
|
package/index.js
CHANGED
package/kan2.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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.
|