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.
- kanban_framework-0.13.1/PKG-INFO +8 -0
- kanban_framework-0.13.1/core/__init__.py +3 -0
- kanban_framework-0.13.1/core/__main__.py +18 -0
- kanban_framework-0.13.1/core/cli/__init__.py +1 -0
- kanban_framework-0.13.1/core/cli/evaluator.py +133 -0
- kanban_framework-0.13.1/core/cli/framework.py +94 -0
- kanban_framework-0.13.1/core/cli/inbox.py +205 -0
- kanban_framework-0.13.1/core/cli/knowledge.py +261 -0
- kanban_framework-0.13.1/core/cli/main.py +158 -0
- kanban_framework-0.13.1/core/cli/plan.py +76 -0
- kanban_framework-0.13.1/core/cli/query.py +142 -0
- kanban_framework-0.13.1/core/cli/run.py +800 -0
- kanban_framework-0.13.1/core/cli/skills.py +23 -0
- kanban_framework-0.13.1/core/cli/subtask.py +215 -0
- kanban_framework-0.13.1/core/cli/task.py +346 -0
- kanban_framework-0.13.1/core/cli/version.py +55 -0
- kanban_framework-0.13.1/core/domain/__init__.py +13 -0
- kanban_framework-0.13.1/core/domain/evaluation.py +29 -0
- kanban_framework-0.13.1/core/domain/guard.py +439 -0
- kanban_framework-0.13.1/core/domain/inbox_analyzer.py +218 -0
- kanban_framework-0.13.1/core/domain/knowledge.py +310 -0
- kanban_framework-0.13.1/core/domain/nlp.py +51 -0
- kanban_framework-0.13.1/core/domain/progress.py +83 -0
- kanban_framework-0.13.1/core/domain/recovery.py +100 -0
- kanban_framework-0.13.1/core/domain/scanner.py +505 -0
- kanban_framework-0.13.1/core/domain/self_improve.py +47 -0
- kanban_framework-0.13.1/core/domain/skills.py +41 -0
- kanban_framework-0.13.1/core/domain/task.py +229 -0
- kanban_framework-0.13.1/core/domain/version.py +22 -0
- kanban_framework-0.13.1/core/domain/workflow.py +154 -0
- kanban_framework-0.13.1/core/infra/__init__.py +1 -0
- kanban_framework-0.13.1/core/infra/config.py +72 -0
- kanban_framework-0.13.1/core/infra/consts.py +22 -0
- kanban_framework-0.13.1/core/infra/dashboard.py +31 -0
- kanban_framework-0.13.1/core/infra/filesystem.py +171 -0
- kanban_framework-0.13.1/core/infra/git.py +74 -0
- kanban_framework-0.13.1/core/infra/scheduler.py +103 -0
- kanban_framework-0.13.1/core/infra/time_tracking.py +54 -0
- kanban_framework-0.13.1/core/infra/token_tracking.py +38 -0
- kanban_framework-0.13.1/core/infra/worktree.py +64 -0
- kanban_framework-0.13.1/core/types.py +148 -0
- kanban_framework-0.13.1/kanban_framework.egg-info/PKG-INFO +8 -0
- kanban_framework-0.13.1/kanban_framework.egg-info/SOURCES.txt +50 -0
- kanban_framework-0.13.1/kanban_framework.egg-info/dependency_links.txt +1 -0
- kanban_framework-0.13.1/kanban_framework.egg-info/entry_points.txt +2 -0
- kanban_framework-0.13.1/kanban_framework.egg-info/requires.txt +4 -0
- kanban_framework-0.13.1/kanban_framework.egg-info/top_level.txt +1 -0
- kanban_framework-0.13.1/pyproject.toml +31 -0
- kanban_framework-0.13.1/setup.cfg +4 -0
- kanban_framework-0.13.1/test/test_knowledge_integration.py +66 -0
- kanban_framework-0.13.1/test/test_plan_parser.py +116 -0
- kanban_framework-0.13.1/test/test_types.py +239 -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])
|