aiwcli 0.9.7 → 0.10.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/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +49 -18
- package/dist/templates/_shared/.claude/settings.json +4 -0
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +87 -178
- package/dist/templates/_shared/hooks/context_monitor.py +128 -194
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +104 -0
- package/dist/templates/_shared/hooks/session_end.py +154 -0
- package/dist/templates/_shared/hooks/session_start.py +145 -59
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
- package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
- package/dist/templates/_shared/lib/base/__init__.py +16 -0
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/constants.py +18 -4
- package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
- package/dist/templates/_shared/lib/base/inference.py +121 -0
- package/dist/templates/_shared/lib/base/logger.py +291 -0
- package/dist/templates/_shared/lib/base/utils.py +49 -11
- package/dist/templates/_shared/lib/context/__init__.py +72 -80
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
- package/dist/templates/_shared/lib/context/context_selector.py +491 -0
- package/dist/templates/_shared/lib/context/context_store.py +636 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
- package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
- package/dist/templates/_shared/lib/templates/README.md +5 -13
- package/dist/templates/_shared/lib/templates/__init__.py +2 -6
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +25 -79
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +39 -19
- package/dist/templates/_shared/scripts/status_line.py +701 -0
- package/dist/templates/_shared/workflows/handoff.md +9 -3
- package/dist/templates/cc-native/.claude/settings.json +64 -9
- package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
- package/dist/templates/cc-native/MIGRATION.md +1 -1
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -57
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
- package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +35 -10
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +103 -42
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
- package/dist/templates/cc-native/_cc-native/lib/utils.py +210 -43
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
- package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -205
- package/dist/templates/_shared/lib/context/cache.py +0 -444
- package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
- package/dist/templates/_shared/lib/context/context_manager.py +0 -1054
- package/dist/templates/_shared/lib/context/discovery.py +0 -444
- package/dist/templates/_shared/lib/context/event_log.py +0 -308
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -290
- package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Plan lifecycle management — archival, lookup, and path extraction.
|
|
2
|
+
|
|
3
|
+
Provides pure-data operations on plan files:
|
|
4
|
+
- archive_plan: copy plan to context plans/ folder, compute hash + signature
|
|
5
|
+
- find_latest_plan: locate the most relevant plan for a context
|
|
6
|
+
- extract_plan_path_from_result: parse plan path from ExitPlanMode output
|
|
7
|
+
|
|
8
|
+
This module does NOT modify mode or state.json. The calling hook
|
|
9
|
+
(e.g. archive_plan.py) is responsible for updating mode via
|
|
10
|
+
context_store.update_mode() after archival succeeds.
|
|
11
|
+
"""
|
|
12
|
+
import hashlib
|
|
13
|
+
import re
|
|
14
|
+
import uuid
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from ..base.atomic_write import atomic_write
|
|
20
|
+
from ..base.constants import get_context_dir, get_context_plans_dir
|
|
21
|
+
from ..base.logger import log_debug, log_info, log_warn, log_error
|
|
22
|
+
from ..base.utils import sanitize_title
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Plan archival
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def archive_plan(
|
|
30
|
+
plan_path: str,
|
|
31
|
+
context_id: str,
|
|
32
|
+
project_root: Path = None,
|
|
33
|
+
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
34
|
+
"""Archive a plan file to the context's plans/ folder.
|
|
35
|
+
|
|
36
|
+
Copies the plan content to:
|
|
37
|
+
_output/contexts/{context_id}/plans/{date}-{slug}.md
|
|
38
|
+
|
|
39
|
+
Computes a content hash and signature for change detection and
|
|
40
|
+
fallback matching after /clear.
|
|
41
|
+
|
|
42
|
+
Does NOT modify state.json or mode — the calling hook handles that
|
|
43
|
+
via context_store.update_mode().
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
plan_path: Path to the source plan file.
|
|
47
|
+
context_id: Target context identifier.
|
|
48
|
+
project_root: Project root directory (default: from env / cwd).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
(archived_path, plan_hash, plan_signature) on success.
|
|
52
|
+
(None, None, None) on any error.
|
|
53
|
+
"""
|
|
54
|
+
plan_file = Path(plan_path)
|
|
55
|
+
if not plan_file.exists():
|
|
56
|
+
log_warn("plan_manager", f"Plan file not found: {plan_path}")
|
|
57
|
+
return None, None, None
|
|
58
|
+
|
|
59
|
+
# Read plan content
|
|
60
|
+
try:
|
|
61
|
+
content = plan_file.read_text(encoding="utf-8")
|
|
62
|
+
except Exception as e:
|
|
63
|
+
log_error("plan_manager", f"Failed to read plan: {e}")
|
|
64
|
+
return None, None, None
|
|
65
|
+
|
|
66
|
+
# Compute hash and signature
|
|
67
|
+
plan_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
|
|
68
|
+
plan_signature = content[:200]
|
|
69
|
+
|
|
70
|
+
# Ensure plans directory exists
|
|
71
|
+
plans_dir = get_context_plans_dir(context_id, project_root)
|
|
72
|
+
plans_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
# Generate archive filename: YYYY-MM-DD-<slug>.md
|
|
75
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
76
|
+
slug = sanitize_title(plan_file.stem, max_len=30)
|
|
77
|
+
archive_name = f"{date_str}-{slug}.md"
|
|
78
|
+
archive_path = plans_dir / archive_name
|
|
79
|
+
|
|
80
|
+
# Handle filename collisions with counter suffix
|
|
81
|
+
counter = 2
|
|
82
|
+
while archive_path.exists():
|
|
83
|
+
archive_name = f"{date_str}-{slug}-{counter}.md"
|
|
84
|
+
archive_path = plans_dir / archive_name
|
|
85
|
+
counter += 1
|
|
86
|
+
|
|
87
|
+
# Write archived plan atomically
|
|
88
|
+
success, error = atomic_write(archive_path, content)
|
|
89
|
+
if not success:
|
|
90
|
+
log_error("plan_manager", f"Failed to write archive: {error}")
|
|
91
|
+
return None, None, None
|
|
92
|
+
|
|
93
|
+
log_info("plan_manager", f"Archived plan to: {archive_path}")
|
|
94
|
+
return str(archive_path), plan_hash, plan_signature
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Plan lookup
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def find_latest_plan(
|
|
102
|
+
context_id: str,
|
|
103
|
+
project_root: Path = None,
|
|
104
|
+
) -> Optional[str]:
|
|
105
|
+
"""Find the most relevant plan file for a context.
|
|
106
|
+
|
|
107
|
+
Priority:
|
|
108
|
+
1. state.json plan_path — if the file still exists on disk.
|
|
109
|
+
2. Most recent .md in plans/ directory by modification time.
|
|
110
|
+
3. None if no plans found.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
context_id: Context identifier.
|
|
114
|
+
project_root: Project root directory (default: from env / cwd).
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Absolute path string to the plan file, or None.
|
|
118
|
+
"""
|
|
119
|
+
# 1. Check state.json plan_path first
|
|
120
|
+
try:
|
|
121
|
+
from .context_store import load_state
|
|
122
|
+
state = load_state(context_id, project_root)
|
|
123
|
+
if state and state.plan_path:
|
|
124
|
+
plan_path = Path(state.plan_path)
|
|
125
|
+
if plan_path.exists():
|
|
126
|
+
return str(plan_path)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
log_warn("plan_manager", f"Failed to check state.json plan_path: {e}")
|
|
129
|
+
|
|
130
|
+
# 2. Fall back to most recent .md in plans/ dir by mtime
|
|
131
|
+
plans_dir = get_context_plans_dir(context_id, project_root)
|
|
132
|
+
if plans_dir.exists():
|
|
133
|
+
plans = sorted(
|
|
134
|
+
plans_dir.glob("*.md"),
|
|
135
|
+
key=lambda p: p.stat().st_mtime,
|
|
136
|
+
reverse=True,
|
|
137
|
+
)
|
|
138
|
+
if plans:
|
|
139
|
+
return str(plans[0])
|
|
140
|
+
|
|
141
|
+
# 3. No plan found
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Plan identification and normalization
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def generate_plan_id() -> str:
|
|
150
|
+
"""Generate a short unique plan identifier (8 hex chars)."""
|
|
151
|
+
return uuid.uuid4().hex[:8]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def normalize_plan_content(text: str) -> str:
|
|
155
|
+
"""Aggressively normalize plan content for hashing.
|
|
156
|
+
|
|
157
|
+
Strips all XML/HTML tags and collapses whitespace so that
|
|
158
|
+
wrapper variations (e.g. <system-reminder>) don't affect the hash.
|
|
159
|
+
"""
|
|
160
|
+
text = re.sub(r'<[^>]+>', '', text)
|
|
161
|
+
text = re.sub(r'\s+', ' ', text).strip()
|
|
162
|
+
return text
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def extract_plan_anchors(content: str, max_anchors: int = 5) -> List[str]:
|
|
166
|
+
"""Extract structural anchors from plan content.
|
|
167
|
+
|
|
168
|
+
Returns markdown headings + first substantial paragraph as short strings.
|
|
169
|
+
Used for fuzzy matching when hash-based matching fails.
|
|
170
|
+
"""
|
|
171
|
+
anchors = []
|
|
172
|
+
for line in content.splitlines():
|
|
173
|
+
line = line.strip()
|
|
174
|
+
if line.startswith('#') and len(line) > 3:
|
|
175
|
+
anchors.append(line[:80])
|
|
176
|
+
elif not anchors and len(line) > 20:
|
|
177
|
+
anchors.append(line[:80])
|
|
178
|
+
if len(anchors) >= max_anchors:
|
|
179
|
+
break
|
|
180
|
+
return anchors
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# Path extraction from tool output
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def extract_plan_path_from_result(tool_result: str) -> Optional[str]:
|
|
188
|
+
"""Extract plan file path from ExitPlanMode tool result.
|
|
189
|
+
|
|
190
|
+
Parses the pattern: "Your plan has been saved to: <path>"
|
|
191
|
+
from the tool_result string returned by ExitPlanMode.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
tool_result: Raw text output from the ExitPlanMode tool.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Plan file path string (stripped), or None if not found.
|
|
198
|
+
"""
|
|
199
|
+
if not tool_result:
|
|
200
|
+
return None
|
|
201
|
+
match = re.search(r"Your plan has been saved to:\s*(.+\.md)", tool_result)
|
|
202
|
+
if match:
|
|
203
|
+
return match.group(1).strip()
|
|
204
|
+
return None
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Task tracker — direct state.json CRUD for tasks.
|
|
2
|
+
|
|
3
|
+
Writes tasks directly to the tasks[] array in state.json,
|
|
4
|
+
bypassing events.jsonl for faster, simpler task operations.
|
|
5
|
+
|
|
6
|
+
All functions do their own I/O to avoid circular imports with
|
|
7
|
+
context_store.py.
|
|
8
|
+
"""
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from ..base.atomic_write import atomic_write
|
|
15
|
+
from ..base.constants import get_context_dir
|
|
16
|
+
from ..base.logger import log_warn
|
|
17
|
+
from ..base.utils import now_iso
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Internal I/O (avoids circular import with context_store)
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
def _state_path(context_id: str, project_root: Path = None) -> Path:
|
|
25
|
+
return get_context_dir(context_id, project_root) / "state.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_state(context_id: str, project_root: Path = None) -> Optional[dict]:
|
|
29
|
+
sp = _state_path(context_id, project_root)
|
|
30
|
+
if not sp.exists():
|
|
31
|
+
return None
|
|
32
|
+
try:
|
|
33
|
+
return json.loads(sp.read_text(encoding="utf-8"))
|
|
34
|
+
except Exception as e:
|
|
35
|
+
log_warn("task_tracker", f"Failed to read state.json: {e}")
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _save_state(context_id: str, state_data: dict, project_root: Path = None) -> bool:
|
|
40
|
+
sp = _state_path(context_id, project_root)
|
|
41
|
+
content = json.dumps(state_data, indent=2, ensure_ascii=False)
|
|
42
|
+
success, error = atomic_write(sp, content)
|
|
43
|
+
if not success:
|
|
44
|
+
log_warn("task_tracker", f"Failed to write state.json: {error}")
|
|
45
|
+
return success
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Public API
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def generate_next_task_id(context_id: str, project_root: Path = None) -> str:
|
|
53
|
+
"""Scan tasks[] for highest aiw-N, return aiw-(N+1)."""
|
|
54
|
+
state = _load_state(context_id, project_root)
|
|
55
|
+
tasks = state.get("tasks", []) if state else []
|
|
56
|
+
|
|
57
|
+
max_num = 0
|
|
58
|
+
for t in tasks:
|
|
59
|
+
tid = t.get("id", "")
|
|
60
|
+
m = re.match(r"^aiw-(\d+)$", tid)
|
|
61
|
+
if m:
|
|
62
|
+
max_num = max(max_num, int(m.group(1)))
|
|
63
|
+
|
|
64
|
+
return f"aiw-{max_num + 1}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def add_task(
|
|
68
|
+
context_id: str,
|
|
69
|
+
subject: str,
|
|
70
|
+
description: str = "",
|
|
71
|
+
active_form: str = "",
|
|
72
|
+
session_id: str = "",
|
|
73
|
+
project_root: Path = None,
|
|
74
|
+
) -> Optional[dict]:
|
|
75
|
+
"""Add a new task to state.json tasks[] and return the task dict."""
|
|
76
|
+
state = _load_state(context_id, project_root)
|
|
77
|
+
if state is None:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
task_id = generate_next_task_id(context_id, project_root)
|
|
81
|
+
task = {
|
|
82
|
+
"id": task_id,
|
|
83
|
+
"subject": subject,
|
|
84
|
+
"description": description,
|
|
85
|
+
"active_form": active_form,
|
|
86
|
+
"status": "pending",
|
|
87
|
+
"created_at": now_iso(),
|
|
88
|
+
"completed_at": None,
|
|
89
|
+
"evidence": "",
|
|
90
|
+
"work_summary": "",
|
|
91
|
+
"files_changed": [],
|
|
92
|
+
"session_id": session_id,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
state.setdefault("tasks", []).append(task)
|
|
96
|
+
state["last_active"] = now_iso()
|
|
97
|
+
|
|
98
|
+
if _save_state(context_id, state, project_root):
|
|
99
|
+
return task
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def update_task(
|
|
104
|
+
context_id: str,
|
|
105
|
+
task_id: str,
|
|
106
|
+
status: str = None,
|
|
107
|
+
evidence: str = "",
|
|
108
|
+
work_summary: str = "",
|
|
109
|
+
files_changed: List[str] = None,
|
|
110
|
+
session_id: str = "",
|
|
111
|
+
project_root: Path = None,
|
|
112
|
+
) -> bool:
|
|
113
|
+
"""Find task by task_id in tasks[], update fields, return True on success."""
|
|
114
|
+
state = _load_state(context_id, project_root)
|
|
115
|
+
if state is None:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
for task in state.get("tasks", []):
|
|
119
|
+
if task.get("id") == task_id:
|
|
120
|
+
if status is not None:
|
|
121
|
+
task["status"] = status
|
|
122
|
+
if status == "completed":
|
|
123
|
+
task["completed_at"] = now_iso()
|
|
124
|
+
if evidence:
|
|
125
|
+
task["evidence"] = evidence
|
|
126
|
+
if work_summary:
|
|
127
|
+
task["work_summary"] = work_summary
|
|
128
|
+
if files_changed is not None:
|
|
129
|
+
task["files_changed"] = files_changed
|
|
130
|
+
if session_id:
|
|
131
|
+
task["session_id"] = session_id
|
|
132
|
+
state["last_active"] = now_iso()
|
|
133
|
+
return _save_state(context_id, state, project_root)
|
|
134
|
+
|
|
135
|
+
log_warn("task_tracker", f"Task '{task_id}' not found in context '{context_id}'")
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def delete_task(context_id: str, task_id: str, project_root: Path = None) -> bool:
|
|
140
|
+
"""Remove task from tasks[] and return True on success."""
|
|
141
|
+
state = _load_state(context_id, project_root)
|
|
142
|
+
if state is None:
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
tasks = state.get("tasks", [])
|
|
146
|
+
original_len = len(tasks)
|
|
147
|
+
state["tasks"] = [t for t in tasks if t.get("id") != task_id]
|
|
148
|
+
|
|
149
|
+
if len(state["tasks"]) == original_len:
|
|
150
|
+
log_warn("task_tracker", f"Task '{task_id}' not found in context '{context_id}'")
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
state["last_active"] = now_iso()
|
|
154
|
+
return _save_state(context_id, state, project_root)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_tasks(context_id: str, project_root: Path = None) -> List[dict]:
|
|
158
|
+
"""Return tasks[] from state.json."""
|
|
159
|
+
state = _load_state(context_id, project_root)
|
|
160
|
+
if state is None:
|
|
161
|
+
return []
|
|
162
|
+
return state.get("tasks", [])
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def generate_task_summary(context_id: str, project_root: Path = None) -> str:
|
|
166
|
+
"""Partition tasks and format as markdown checklist."""
|
|
167
|
+
tasks = get_tasks(context_id, project_root)
|
|
168
|
+
if not tasks:
|
|
169
|
+
return "No tasks in this context."
|
|
170
|
+
|
|
171
|
+
completed = [t for t in tasks if t.get("status") == "completed"]
|
|
172
|
+
in_progress = [t for t in tasks if t.get("status") == "in_progress"]
|
|
173
|
+
pending = [t for t in tasks if t.get("status") == "pending"]
|
|
174
|
+
blocked = [t for t in tasks if t.get("status") == "blocked"]
|
|
175
|
+
|
|
176
|
+
lines = [f"### Tasks ({len(tasks)} total)", ""]
|
|
177
|
+
|
|
178
|
+
for t in completed:
|
|
179
|
+
ws = f"\n Work: {t['work_summary']}" if t.get("work_summary") else ""
|
|
180
|
+
lines.append(f"- [x] {t['id']}: {t['subject']}{ws}")
|
|
181
|
+
for t in in_progress:
|
|
182
|
+
lines.append(f"- [~] {t['id']}: {t['subject']}")
|
|
183
|
+
for t in pending:
|
|
184
|
+
lines.append(f"- [ ] {t['id']}: {t['subject']}")
|
|
185
|
+
for t in blocked:
|
|
186
|
+
lines.append(f"- [!] {t['id']}: {t['subject']}")
|
|
187
|
+
|
|
188
|
+
return "\n".join(lines)
|
|
Binary file
|
|
@@ -18,14 +18,10 @@ from typing import Any, Dict, List, Optional
|
|
|
18
18
|
|
|
19
19
|
from ..base.atomic_write import atomic_write
|
|
20
20
|
from ..base.constants import get_context_handoffs_dir, get_context_dir
|
|
21
|
-
from ..base.
|
|
22
|
-
from ..
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
get_pending_tasks,
|
|
26
|
-
Task,
|
|
27
|
-
EVENT_HANDOFF_CREATED,
|
|
28
|
-
)
|
|
21
|
+
from ..base.logger import log_info, log_error
|
|
22
|
+
from ..base.utils import now_iso
|
|
23
|
+
from ..context.context_store import get_context as _get_context_state, save_state as _save_state
|
|
24
|
+
from ..context.task_tracker import get_tasks
|
|
29
25
|
from ..templates.formatters import render_task_list, format_continuation_header, format_reason
|
|
30
26
|
|
|
31
27
|
|
|
@@ -83,19 +79,17 @@ def generate_handoff_document(
|
|
|
83
79
|
Returns:
|
|
84
80
|
HandoffDocument with file_path set, or None on failure
|
|
85
81
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
context = get_context(context_id, project_root)
|
|
82
|
+
context = _get_context_state(context_id, project_root)
|
|
89
83
|
if not context:
|
|
90
|
-
|
|
84
|
+
log_error("handoff", f"Context '{context_id}' not found")
|
|
91
85
|
return None
|
|
92
86
|
|
|
93
87
|
# Generate session ID
|
|
94
88
|
session_id = str(uuid.uuid4())[:8]
|
|
95
89
|
|
|
96
|
-
# Get
|
|
97
|
-
|
|
98
|
-
pending_tasks =
|
|
90
|
+
# Get pending tasks from state.json
|
|
91
|
+
all_tasks = get_tasks(context_id, project_root)
|
|
92
|
+
pending_tasks = [t for t in all_tasks if t.get("status") in ("pending", "in_progress", "blocked")]
|
|
99
93
|
|
|
100
94
|
# Build document
|
|
101
95
|
now = now_iso()
|
|
@@ -107,10 +101,10 @@ def generate_handoff_document(
|
|
|
107
101
|
session_id=session_id,
|
|
108
102
|
reason=reason,
|
|
109
103
|
created_at=now,
|
|
110
|
-
plan_path=context.
|
|
104
|
+
plan_path=context.plan_path,
|
|
111
105
|
context_folder=str(context_dir),
|
|
112
|
-
events_log_path=str(context_dir / "
|
|
113
|
-
active_tasks=
|
|
106
|
+
events_log_path=str(context_dir / "state.json"),
|
|
107
|
+
active_tasks=pending_tasks,
|
|
114
108
|
completed_tasks_this_session=[
|
|
115
109
|
{"subject": s} for s in (completed_this_session or [])
|
|
116
110
|
],
|
|
@@ -138,33 +132,13 @@ def generate_handoff_document(
|
|
|
138
132
|
|
|
139
133
|
success, error = atomic_write(file_path, markdown)
|
|
140
134
|
if not success:
|
|
141
|
-
|
|
135
|
+
log_error("handoff", f"Failed to write handoff document: {error}")
|
|
142
136
|
return None
|
|
143
137
|
|
|
144
|
-
|
|
145
|
-
append_event(
|
|
146
|
-
context_id,
|
|
147
|
-
EVENT_HANDOFF_CREATED,
|
|
148
|
-
project_root,
|
|
149
|
-
path=str(file_path),
|
|
150
|
-
reason=reason,
|
|
151
|
-
session_id=session_id
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
eprint(f"[handoff] Created handoff document: {file_path}")
|
|
138
|
+
log_info("handoff", f"Created handoff document: {file_path}")
|
|
155
139
|
return doc
|
|
156
140
|
|
|
157
141
|
|
|
158
|
-
def _task_to_dict(task: Task) -> Dict[str, Any]:
|
|
159
|
-
"""Convert Task to dictionary for handoff document."""
|
|
160
|
-
return {
|
|
161
|
-
"id": task.id,
|
|
162
|
-
"subject": task.subject,
|
|
163
|
-
"status": task.status,
|
|
164
|
-
"description": task.description,
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
142
|
def _render_handoff_markdown(doc: HandoffDocument) -> str:
|
|
169
143
|
"""Render handoff document as markdown."""
|
|
170
144
|
lines = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Shared Templates Module
|
|
2
2
|
|
|
3
|
-
Centralized templates and formatters for consistent context management output across
|
|
3
|
+
Centralized templates and formatters for consistent context management output across context formatting, handoff, and hooks.
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
@@ -115,10 +115,6 @@ Plan context templates for the add_plan_context hook.
|
|
|
115
115
|
|
|
116
116
|
Returns the evaluation context reminder template that prompts Claude to add evaluation context to plans.
|
|
117
117
|
|
|
118
|
-
**`get_questions_offer_template() -> str`**
|
|
119
|
-
|
|
120
|
-
Returns the clarifying questions offer template shown on first plan write.
|
|
121
|
-
|
|
122
118
|
## Usage
|
|
123
119
|
|
|
124
120
|
### In Discovery Functions
|
|
@@ -158,28 +154,24 @@ reason_text = format_reason(doc.reason)
|
|
|
158
154
|
### In Hooks
|
|
159
155
|
|
|
160
156
|
```python
|
|
161
|
-
from templates.plan_context import
|
|
162
|
-
get_evaluation_context_reminder,
|
|
163
|
-
get_questions_offer_template,
|
|
164
|
-
)
|
|
157
|
+
from templates.plan_context import get_evaluation_context_reminder
|
|
165
158
|
|
|
166
|
-
# Get plan context
|
|
159
|
+
# Get plan context template
|
|
167
160
|
CONTEXT_REMINDER = get_evaluation_context_reminder()
|
|
168
|
-
QUESTIONS_OFFER = get_questions_offer_template()
|
|
169
161
|
```
|
|
170
162
|
|
|
171
163
|
## Dependent Files
|
|
172
164
|
|
|
173
165
|
Files that import from this module:
|
|
174
166
|
|
|
175
|
-
- `.aiwcli/_shared/lib/context/
|
|
167
|
+
- `.aiwcli/_shared/lib/context/context_formatter.py`
|
|
176
168
|
- Uses: `get_mode_display`, `get_status_icon`, `format_continuation_header`
|
|
177
169
|
|
|
178
170
|
- `.aiwcli/_shared/lib/handoff/document_generator.py`
|
|
179
171
|
- Uses: `render_task_list`, `format_continuation_header`, `format_reason`
|
|
180
172
|
|
|
181
173
|
- `.aiwcli/_cc-native/hooks/add_plan_context.py`
|
|
182
|
-
- Uses: `get_evaluation_context_reminder
|
|
174
|
+
- Uses: `get_evaluation_context_reminder`
|
|
183
175
|
|
|
184
176
|
## Design Principles
|
|
185
177
|
|
|
@@ -6,7 +6,7 @@ This module provides centralized templates for:
|
|
|
6
6
|
- Task rendering
|
|
7
7
|
- Context continuation headers
|
|
8
8
|
|
|
9
|
-
Used by
|
|
9
|
+
Used by context_formatter.py, document_generator.py, and hooks.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from .formatters import (
|
|
@@ -20,10 +20,7 @@ from .formatters import (
|
|
|
20
20
|
format_continuation_header,
|
|
21
21
|
format_reason,
|
|
22
22
|
)
|
|
23
|
-
from .plan_context import
|
|
24
|
-
get_evaluation_context_reminder,
|
|
25
|
-
get_questions_offer_template,
|
|
26
|
-
)
|
|
23
|
+
from .plan_context import get_evaluation_context_reminder
|
|
27
24
|
|
|
28
25
|
__all__ = [
|
|
29
26
|
"MODE_DISPLAY_MAP",
|
|
@@ -36,5 +33,4 @@ __all__ = [
|
|
|
36
33
|
"format_continuation_header",
|
|
37
34
|
"format_reason",
|
|
38
35
|
"get_evaluation_context_reminder",
|
|
39
|
-
"get_questions_offer_template",
|
|
40
36
|
]
|
|
Binary file
|
|
Binary file
|