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.
Files changed (119) hide show
  1. package/bin/run.js +5 -2
  2. package/dist/lib/claude-settings-types.d.ts +2 -0
  3. package/dist/templates/CLAUDE.md +49 -18
  4. package/dist/templates/_shared/.claude/settings.json +4 -0
  5. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  11. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  18. package/dist/templates/_shared/hooks/context_monitor.py +128 -194
  19. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  20. package/dist/templates/_shared/hooks/pre_compact.py +104 -0
  21. package/dist/templates/_shared/hooks/session_end.py +154 -0
  22. package/dist/templates/_shared/hooks/session_start.py +145 -59
  23. package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
  24. package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
  25. package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
  26. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  32. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  33. package/dist/templates/_shared/lib/base/constants.py +18 -4
  34. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  35. package/dist/templates/_shared/lib/base/inference.py +121 -0
  36. package/dist/templates/_shared/lib/base/logger.py +291 -0
  37. package/dist/templates/_shared/lib/base/utils.py +49 -11
  38. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  39. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  45. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  46. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  47. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  48. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  49. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  50. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  51. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  52. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  54. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  55. package/dist/templates/_shared/lib/templates/README.md +5 -13
  56. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  57. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  58. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/lib/templates/plan_context.py +25 -79
  61. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  62. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  63. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  64. package/dist/templates/_shared/scripts/status_line.py +701 -0
  65. package/dist/templates/_shared/workflows/handoff.md +9 -3
  66. package/dist/templates/cc-native/.claude/settings.json +64 -9
  67. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  68. package/dist/templates/cc-native/MIGRATION.md +1 -1
  69. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  70. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
  71. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  75. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  76. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -57
  79. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
  80. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  81. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  82. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  83. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +35 -10
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  87. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  91. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +103 -42
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  96. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  97. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  98. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  99. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  100. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  101. package/dist/templates/cc-native/_cc-native/lib/utils.py +210 -43
  102. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  103. package/oclif.manifest.json +1 -1
  104. package/package.json +1 -1
  105. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  106. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -205
  107. package/dist/templates/_shared/lib/context/cache.py +0 -444
  108. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  109. package/dist/templates/_shared/lib/context/context_manager.py +0 -1054
  110. package/dist/templates/_shared/lib/context/discovery.py +0 -444
  111. package/dist/templates/_shared/lib/context/event_log.py +0 -308
  112. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  113. package/dist/templates/_shared/lib/context/task_sync.py +0 -290
  114. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  115. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  116. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  117. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  118. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  119. 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)
@@ -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.utils import eprint, now_iso
22
- from ..context.event_log import (
23
- append_event,
24
- get_current_state,
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
- from ..context.context_manager import get_context
87
-
88
- context = get_context(context_id, project_root)
82
+ context = _get_context_state(context_id, project_root)
89
83
  if not context:
90
- eprint(f"[handoff] ERROR: Context '{context_id}' not found")
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 current state
97
- state = get_current_state(context_id, project_root)
98
- pending_tasks = get_pending_tasks(context_id, project_root)
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.in_flight.artifact_path if context.in_flight else None,
104
+ plan_path=context.plan_path,
111
105
  context_folder=str(context_dir),
112
- events_log_path=str(context_dir / "events.jsonl"),
113
- active_tasks=[_task_to_dict(t) for t in pending_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
- eprint(f"[handoff] ERROR: Failed to write handoff document: {error}")
135
+ log_error("handoff", f"Failed to write handoff document: {error}")
142
136
  return None
143
137
 
144
- # Record event (informational only - no mode change)
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 discovery, handoff, and hooks.
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 templates
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/discovery.py`
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`, `get_questions_offer_template`
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 discovery.py, document_generator.py, and hooks.
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
  ]