kanban-framework 0.13.1__tar.gz

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.
Files changed (52) hide show
  1. kanban_framework-0.13.1/PKG-INFO +8 -0
  2. kanban_framework-0.13.1/core/__init__.py +3 -0
  3. kanban_framework-0.13.1/core/__main__.py +18 -0
  4. kanban_framework-0.13.1/core/cli/__init__.py +1 -0
  5. kanban_framework-0.13.1/core/cli/evaluator.py +133 -0
  6. kanban_framework-0.13.1/core/cli/framework.py +94 -0
  7. kanban_framework-0.13.1/core/cli/inbox.py +205 -0
  8. kanban_framework-0.13.1/core/cli/knowledge.py +261 -0
  9. kanban_framework-0.13.1/core/cli/main.py +158 -0
  10. kanban_framework-0.13.1/core/cli/plan.py +76 -0
  11. kanban_framework-0.13.1/core/cli/query.py +142 -0
  12. kanban_framework-0.13.1/core/cli/run.py +800 -0
  13. kanban_framework-0.13.1/core/cli/skills.py +23 -0
  14. kanban_framework-0.13.1/core/cli/subtask.py +215 -0
  15. kanban_framework-0.13.1/core/cli/task.py +346 -0
  16. kanban_framework-0.13.1/core/cli/version.py +55 -0
  17. kanban_framework-0.13.1/core/domain/__init__.py +13 -0
  18. kanban_framework-0.13.1/core/domain/evaluation.py +29 -0
  19. kanban_framework-0.13.1/core/domain/guard.py +439 -0
  20. kanban_framework-0.13.1/core/domain/inbox_analyzer.py +218 -0
  21. kanban_framework-0.13.1/core/domain/knowledge.py +310 -0
  22. kanban_framework-0.13.1/core/domain/nlp.py +51 -0
  23. kanban_framework-0.13.1/core/domain/progress.py +83 -0
  24. kanban_framework-0.13.1/core/domain/recovery.py +100 -0
  25. kanban_framework-0.13.1/core/domain/scanner.py +505 -0
  26. kanban_framework-0.13.1/core/domain/self_improve.py +47 -0
  27. kanban_framework-0.13.1/core/domain/skills.py +41 -0
  28. kanban_framework-0.13.1/core/domain/task.py +229 -0
  29. kanban_framework-0.13.1/core/domain/version.py +22 -0
  30. kanban_framework-0.13.1/core/domain/workflow.py +154 -0
  31. kanban_framework-0.13.1/core/infra/__init__.py +1 -0
  32. kanban_framework-0.13.1/core/infra/config.py +72 -0
  33. kanban_framework-0.13.1/core/infra/consts.py +22 -0
  34. kanban_framework-0.13.1/core/infra/dashboard.py +31 -0
  35. kanban_framework-0.13.1/core/infra/filesystem.py +171 -0
  36. kanban_framework-0.13.1/core/infra/git.py +74 -0
  37. kanban_framework-0.13.1/core/infra/scheduler.py +103 -0
  38. kanban_framework-0.13.1/core/infra/time_tracking.py +54 -0
  39. kanban_framework-0.13.1/core/infra/token_tracking.py +38 -0
  40. kanban_framework-0.13.1/core/infra/worktree.py +64 -0
  41. kanban_framework-0.13.1/core/types.py +148 -0
  42. kanban_framework-0.13.1/kanban_framework.egg-info/PKG-INFO +8 -0
  43. kanban_framework-0.13.1/kanban_framework.egg-info/SOURCES.txt +50 -0
  44. kanban_framework-0.13.1/kanban_framework.egg-info/dependency_links.txt +1 -0
  45. kanban_framework-0.13.1/kanban_framework.egg-info/entry_points.txt +2 -0
  46. kanban_framework-0.13.1/kanban_framework.egg-info/requires.txt +4 -0
  47. kanban_framework-0.13.1/kanban_framework.egg-info/top_level.txt +1 -0
  48. kanban_framework-0.13.1/pyproject.toml +31 -0
  49. kanban_framework-0.13.1/setup.cfg +4 -0
  50. kanban_framework-0.13.1/test/test_knowledge_integration.py +66 -0
  51. kanban_framework-0.13.1/test/test_plan_parser.py +116 -0
  52. kanban_framework-0.13.1/test/test_types.py +239 -0
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: kanban-framework
3
+ Version: 0.13.1
4
+ Summary: Kanban multi-agent orchestration framework for Claude Code
5
+ Requires-Python: >=3.9
6
+ Provides-Extra: dev
7
+ Requires-Dist: pytest; extra == "dev"
8
+ Requires-Dist: pytest-cov; extra == "dev"
@@ -0,0 +1,3 @@
1
+ """Kanban multi-agent orchestration framework."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,18 @@
1
+ """Allow running kanban as: python -m core <cmd>
2
+
3
+ Supports two invocation modes:
4
+ 1. From framework root: cd .claude/skills/kanban && python -m core status
5
+ 2. From project root: PYTHONPATH=.claude/skills/kanban python -m core status
6
+ """
7
+ import sys
8
+ import os
9
+
10
+ # Add framework root to sys.path so core module can be imported from any cwd
11
+ # __file__ = .../core/__main__.py -> parent = .../kanban/ (contains core/)
12
+ _framework_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13
+ if _framework_root not in sys.path:
14
+ sys.path.insert(0, _framework_root)
15
+
16
+ from core.cli.main import main
17
+
18
+ main()
@@ -0,0 +1 @@
1
+ from .main import main
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ from core.infra.filesystem import Filesystem
4
+ from core.infra.config import Config
5
+ from core.domain.task import TaskManager
6
+
7
+
8
+ def dispatch(args: list[str]) -> dict:
9
+ if not args:
10
+ return {"error": "subcommand required: collect-scores, record-score, collect-plan-review"}
11
+ sub = args[0]
12
+ task_id = args[1] if len(args) > 1 else "unknown"
13
+ root = Filesystem.find_project_root()
14
+ fs = Filesystem(root=root)
15
+ cfg = Config(fs)
16
+ tm = TaskManager(fs, cfg)
17
+
18
+ if sub == "collect-scores":
19
+ return _collect_scores(fs, tm, task_id)
20
+ if sub == "record-score":
21
+ return _record_score(fs, tm, task_id)
22
+ if sub == "collect-plan-review":
23
+ return _collect_plan_review_scores(fs, tm, task_id)
24
+ return {"error": f"unknown evaluator subcommand: {sub}"}
25
+
26
+
27
+ def _collect_scores(fs: Filesystem, tm: TaskManager, task_id: str) -> dict:
28
+ try:
29
+ task = tm.show(task_id)
30
+ except Exception:
31
+ return {"task_id": task_id, "scores": [], "average": None}
32
+
33
+ scores = []
34
+ for it in range(1, task.iteration + 1):
35
+ report_dir = fs.report_dir(task_id, it)
36
+ if not report_dir.exists():
37
+ continue
38
+
39
+ for role in ["code_reviewer", "qa", "pm", "designer"]:
40
+ rf = report_dir / f"{role}_report.json"
41
+ if fs.file_exists(rf):
42
+ data = fs.read_json(rf)
43
+ scores.append({
44
+ "role": role,
45
+ "iteration": it,
46
+ "total": data.get("total", data.get("score", data.get("overall", 0))),
47
+ })
48
+
49
+ avg = round(sum(s["total"] for s in scores) / len(scores), 2) if scores else None
50
+ return {"task_id": task_id, "scores": scores, "average": avg}
51
+
52
+
53
+ def _record_score(fs: Filesystem, tm: TaskManager, task_id: str) -> dict:
54
+ scores_data = _collect_scores(fs, tm, task_id)
55
+ try:
56
+ task = tm.show(task_id)
57
+ except Exception:
58
+ return {"task_id": task_id, "error": "task not found"}
59
+
60
+ avg = scores_data["average"]
61
+ if avg is None:
62
+ return {"task_id": task_id, "recorded": False, "error": "no scores to record"}
63
+
64
+ # Build per-iteration role scores dict
65
+ role_scores = {s["role"]: s["total"] for s in scores_data["scores"]
66
+ if s["iteration"] == task.iteration}
67
+
68
+ # Append to score_history
69
+ entry = {
70
+ "iteration": task.iteration,
71
+ "average": avg,
72
+ "roles": role_scores,
73
+ }
74
+ new_history = list(task.score_history)
75
+ # Replace existing entry for same iteration; otherwise append
76
+ replaced = False
77
+ for i, h in enumerate(new_history):
78
+ if h.get("iteration") == task.iteration:
79
+ new_history[i] = entry
80
+ replaced = True
81
+ break
82
+ if not replaced:
83
+ new_history.append(entry)
84
+
85
+ tm.update(task_id, scores=role_scores, score_history=new_history)
86
+
87
+ return {
88
+ "task_id": task_id,
89
+ "recorded": True,
90
+ "iteration": task.iteration,
91
+ "average": avg,
92
+ }
93
+
94
+
95
+ _PLAN_REVIEW_DIMENSIONS = [
96
+ "requirement_clarity", "technical_feasibility",
97
+ "task_decomposition", "acceptance_criteria",
98
+ "research_completeness", "parallel_safety",
99
+ ]
100
+
101
+
102
+ def _collect_plan_review_scores(
103
+ fs: Filesystem, tm: TaskManager, task_id: str
104
+ ) -> dict:
105
+ try:
106
+ task = tm.show(task_id)
107
+ except Exception:
108
+ return {"task_id": task_id, "dimensions": [], "average": None}
109
+
110
+ dimensions = []
111
+ for it in range(1, task.iteration + 1):
112
+ report_dir = fs.report_dir(task_id, it)
113
+ if not report_dir.exists():
114
+ continue
115
+
116
+ for dim in _PLAN_REVIEW_DIMENSIONS:
117
+ rf = report_dir / f"{dim}_report.json"
118
+ if fs.file_exists(rf):
119
+ data = fs.read_json(rf)
120
+ applicable = data.get("applicable", True)
121
+ if applicable:
122
+ dimensions.append({
123
+ "dimension": dim,
124
+ "iteration": it,
125
+ "score": data.get("score", 0),
126
+ "findings": data.get("findings", []),
127
+ "issues": data.get("issues", []),
128
+ })
129
+
130
+ avg = round(
131
+ sum(d["score"] for d in dimensions) / len(dimensions), 2
132
+ ) if dimensions else None
133
+ return {"task_id": task_id, "dimensions": dimensions, "average": avg}
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ from core.infra.filesystem import Filesystem
4
+ from core.infra.config import Config
5
+
6
+
7
+ def dispatch(args: list[str]) -> dict:
8
+ sub = args[0] if args else "assess"
9
+ if sub == "assess":
10
+ task_id = args[1] if len(args) > 1 else None
11
+ return cmd_assess(task_id)
12
+ return {"subcommand": sub}
13
+
14
+
15
+ def cmd_assess(task_id: str | None = None) -> dict:
16
+ root = Filesystem.find_project_root()
17
+ fs = Filesystem(root=root)
18
+ cfg = Config(fs)
19
+
20
+ results = {
21
+ "iron_rules": _assess_iron_rules(fs),
22
+ "agent_coverage": _assess_agent_coverage(cfg),
23
+ "artifacts": {},
24
+ }
25
+ if task_id:
26
+ results["artifacts"] = _assess_task_artifacts(fs, task_id)
27
+ results["overall_score"] = _calc_score(results)
28
+ return results
29
+
30
+
31
+ def _assess_iron_rules(fs: Filesystem) -> dict:
32
+ rules_dir = fs.kanban_dir.parent / ".claude" / "skills" / "kanban" / "rules"
33
+ if not rules_dir.exists():
34
+ rules_dir = fs.kanban_dir.parent / ".claude" / "rules"
35
+ rule_files = list(rules_dir.glob("*.md")) if rules_dir.exists() else []
36
+ defined = len(rule_files)
37
+ expected = 8 # R-001 through R-008
38
+ return {
39
+ "rules_defined": defined,
40
+ "rules_expected": expected,
41
+ "coverage": min(defined / expected, 1.0) if expected else 1.0,
42
+ }
43
+
44
+
45
+ def _assess_agent_coverage(cfg: Config) -> dict:
46
+ workflow = cfg.workflow
47
+ phases = workflow.get("phases", [])
48
+ covered_phases = 0
49
+ roles = set()
50
+ for p in phases:
51
+ agents = p.get("agents", [])
52
+ if agents:
53
+ covered_phases += 1
54
+ for a in agents:
55
+ roles.add(a.get("role", ""))
56
+ total_phases = len(phases)
57
+ return {
58
+ "phases_with_agents": covered_phases,
59
+ "total_phases": total_phases,
60
+ "coverage": covered_phases / total_phases if total_phases else 1.0,
61
+ "unique_roles": sorted(roles),
62
+ }
63
+
64
+
65
+ def _assess_task_artifacts(fs: Filesystem, task_id: str) -> dict:
66
+ task_dir = fs.task_dir(task_id)
67
+ expected = [
68
+ "spec.md", "plan.md", "task_breakdown.json",
69
+ "execution_summary.md", "execution_pitfalls.md",
70
+ "execution_decisions.md", "retrospective.md", "acceptance.md",
71
+ ]
72
+ present = []
73
+ missing = []
74
+ for fname in expected:
75
+ if (task_dir / fname).exists():
76
+ present.append(fname)
77
+ else:
78
+ missing.append(fname)
79
+ return {
80
+ "present": present,
81
+ "missing": missing,
82
+ "completeness": len(present) / len(expected) if expected else 1.0,
83
+ }
84
+
85
+
86
+ def _calc_score(results: dict) -> float:
87
+ scores = []
88
+ if "iron_rules" in results:
89
+ scores.append(results["iron_rules"].get("coverage", 0))
90
+ if "agent_coverage" in results:
91
+ scores.append(results["agent_coverage"].get("coverage", 0))
92
+ if results.get("artifacts"):
93
+ scores.append(results["artifacts"].get("completeness", 0))
94
+ return round(sum(scores) / len(scores) * 10, 1) if scores else 0.0
@@ -0,0 +1,205 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from core.infra.filesystem import Filesystem
6
+ from core.domain.task import TaskManager, TaskNotFoundError
7
+ from core.infra.config import Config
8
+
9
+
10
+ class InboxError(Exception):
11
+ pass
12
+
13
+
14
+ def dispatch(args: list[str]) -> dict:
15
+ sub = args[0] if args else "list"
16
+ root = Filesystem.find_project_root()
17
+ fs = Filesystem(root=root)
18
+
19
+ if sub == "list":
20
+ return _list_inbox(fs)
21
+ if sub == "add":
22
+ return _add_to_task_inbox(fs, args[1:])
23
+ if sub == "archive":
24
+ return _archive_task_inbox(fs, args[1:])
25
+ if sub == "analyze":
26
+ return _analyze_task_inbox(fs, args[1:])
27
+ if sub == "process":
28
+ return {"subcommand": sub, "processed": []}
29
+ return {"subcommand": sub}
30
+
31
+
32
+ def cmd_feedback(args: list[str]) -> dict:
33
+ task_id = args[0] if args else None
34
+ if not task_id:
35
+ return {"error": "task_id required"}
36
+ text = " ".join(args[1:]) if len(args) > 1 else ""
37
+
38
+ root = Filesystem.find_project_root()
39
+ fs = Filesystem(root=root)
40
+ inbox_file = fs.inbox_file()
41
+ fs.ensure_dir(inbox_file.parent)
42
+
43
+ entries = []
44
+ if fs.file_exists(inbox_file):
45
+ entries = fs.read_json(inbox_file)
46
+
47
+ entries.append({
48
+ "task_id": task_id,
49
+ "text": text,
50
+ "type": "feedback",
51
+ })
52
+ fs.write_json(inbox_file, entries)
53
+ return {"task_id": task_id, "feedback": text, "saved": True}
54
+
55
+
56
+ def _list_inbox(fs: Filesystem) -> dict:
57
+ inbox_file = fs.inbox_file()
58
+ if not fs.file_exists(inbox_file):
59
+ return {"inbox": [], "count": 0}
60
+ entries = fs.read_json(inbox_file)
61
+ return {"inbox": entries, "count": len(entries)}
62
+
63
+
64
+ def _add_to_task_inbox(fs: Filesystem, args: list[str]) -> dict:
65
+ """Add a feedback item to a task's inbox.md file."""
66
+ if not args:
67
+ raise InboxError("task_id required")
68
+ task_id = args[0]
69
+ text = " ".join(args[1:]) if len(args) > 1 else ""
70
+
71
+ if not text:
72
+ raise InboxError("feedback text required")
73
+
74
+ task_dir = fs.task_dir(task_id)
75
+ inbox_path = task_dir / "inbox.md"
76
+
77
+ # Ensure task directory exists
78
+ fs.ensure_dir(task_dir)
79
+
80
+ # Create or append to inbox.md
81
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
82
+ new_entry = f"- [ ] {text} <!-- {timestamp} -->\n"
83
+
84
+ if fs.file_exists(inbox_path):
85
+ content = inbox_path.read_text(encoding="utf-8")
86
+ # Add entry after header section
87
+ lines = content.split("\n")
88
+ insert_pos = 0
89
+ for i, line in enumerate(lines):
90
+ if line.startswith("#"):
91
+ insert_pos = i + 1
92
+ elif line.strip() and not line.startswith("#"):
93
+ # Found first non-header line, insert before it
94
+ break
95
+ lines.insert(insert_pos, "")
96
+ lines.insert(insert_pos + 1, new_entry)
97
+ inbox_path.write_text("\n".join(lines), encoding="utf-8")
98
+ else:
99
+ # Create new inbox.md with header
100
+ inbox_path.write_text(
101
+ f"# Task Inbox — {task_id}\n\n"
102
+ f"用户反馈和待办事项。\n\n"
103
+ f"{new_entry}",
104
+ encoding="utf-8"
105
+ )
106
+
107
+ return {
108
+ "task_id": task_id,
109
+ "action": "added",
110
+ "text": text,
111
+ "inbox_file": str(inbox_path)
112
+ }
113
+
114
+
115
+ def _archive_task_inbox(fs: Filesystem, args: list[str]) -> dict:
116
+ """Archive completed items from task's inbox.md to inbox-archive.md."""
117
+ if not args:
118
+ raise InboxError("task_id required")
119
+ task_id = args[0]
120
+
121
+ task_dir = fs.task_dir(task_id)
122
+ inbox_path = task_dir / "inbox.md"
123
+ archive_path = task_dir / "inbox-archive.md"
124
+
125
+ if not fs.file_exists(inbox_path):
126
+ return {
127
+ "task_id": task_id,
128
+ "action": "archive",
129
+ "archived_count": 0,
130
+ "message": "inbox.md not found"
131
+ }
132
+
133
+ content = inbox_path.read_text(encoding="utf-8")
134
+ lines = content.split("\n")
135
+
136
+ # Separate completed (checked) and pending (unchecked) items
137
+ archived = []
138
+ remaining = []
139
+ in_items = False
140
+
141
+ for line in lines:
142
+ if line.startswith("- [x]") or line.startswith("* [x]"):
143
+ archived.append(line)
144
+ elif line.startswith("- [ ]") or line.startswith("* [ ]"):
145
+ remaining.append(line)
146
+ else:
147
+ remaining.append(line)
148
+
149
+ if not archived:
150
+ return {
151
+ "task_id": task_id,
152
+ "action": "archive",
153
+ "archived_count": 0,
154
+ "message": "no completed items to archive"
155
+ }
156
+
157
+ # Write archive
158
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
159
+ archive_header = f"\n## Archive — {timestamp}\n\n"
160
+ archive_content = archive_header + "\n".join(archived)
161
+
162
+ if fs.file_exists(archive_path):
163
+ existing = archive_path.read_text(encoding="utf-8")
164
+ archive_path.write_text(existing + archive_content, encoding="utf-8")
165
+ else:
166
+ archive_path.write_text(
167
+ f"# Inbox Archive — {task_id}\n\n"
168
+ f"已归档的用户反馈和待办事项。\n"
169
+ f"{archive_content}",
170
+ encoding="utf-8"
171
+ )
172
+
173
+ # Update inbox.md with only pending items
174
+ inbox_path.write_text("\n".join(remaining), encoding="utf-8")
175
+
176
+ return {
177
+ "task_id": task_id,
178
+ "action": "archived",
179
+ "archived_count": len(archived),
180
+ "pending_count": sum(1 for line in remaining if line.startswith("- [ ]") or line.startswith("* [ ]")),
181
+ "archive_file": str(archive_path)
182
+ }
183
+
184
+
185
+ def _analyze_task_inbox(fs: Filesystem, args: list[str]) -> dict:
186
+ from core.domain.inbox_analyzer import InboxAnalyzer
187
+ if not args:
188
+ return {"error": "task_id required"}
189
+ task_id = args[0]
190
+ analyzer = InboxAnalyzer(fs)
191
+ try:
192
+ analysis = analyzer.generate_analysis(task_id)
193
+ return analysis
194
+ except Exception as e:
195
+ return {"error": str(e)}
196
+
197
+
198
+ def archive_on_task_completion(task_id: str) -> dict:
199
+ """
200
+ Auto-archive inbox items when task is completed/archived.
201
+ Called by cmd_decide when action is approve_and_archive.
202
+ """
203
+ root = Filesystem.find_project_root()
204
+ fs = Filesystem(root=root)
205
+ return _archive_task_inbox(fs, [task_id])