aiwcli 0.9.8 → 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 (116) 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 +3 -3
  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_capture.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  17. package/dist/templates/_shared/hooks/context_monitor.py +104 -247
  18. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  19. package/dist/templates/_shared/hooks/pre_compact.py +47 -32
  20. package/dist/templates/_shared/hooks/session_end.py +103 -60
  21. package/dist/templates/_shared/hooks/session_start.py +110 -81
  22. package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
  23. package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
  24. package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
  25. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  26. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  32. package/dist/templates/_shared/lib/base/inference.py +121 -0
  33. package/dist/templates/_shared/lib/base/logger.py +291 -0
  34. package/dist/templates/_shared/lib/base/utils.py +42 -9
  35. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  36. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  38. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  39. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  45. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  46. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  47. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  48. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  49. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  50. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  51. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  52. package/dist/templates/_shared/lib/templates/README.md +5 -13
  53. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  54. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  56. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  57. package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
  58. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  61. package/dist/templates/_shared/scripts/status_line.py +701 -0
  62. package/dist/templates/_shared/workflows/handoff.md +9 -3
  63. package/dist/templates/cc-native/.claude/settings.json +41 -8
  64. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  65. package/dist/templates/cc-native/MIGRATION.md +1 -1
  66. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  67. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
  68. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  69. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  70. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  71. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -55
  75. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
  76. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  79. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
  80. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  81. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  82. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  83. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  87. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
  88. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  91. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  96. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  97. package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
  98. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  99. package/oclif.manifest.json +1 -1
  100. package/package.json +1 -1
  101. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  102. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
  103. package/dist/templates/_shared/lib/context/auto_state.py +0 -167
  104. package/dist/templates/_shared/lib/context/cache.py +0 -444
  105. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  106. package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
  107. package/dist/templates/_shared/lib/context/discovery.py +0 -554
  108. package/dist/templates/_shared/lib/context/event_log.py +0 -316
  109. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  110. package/dist/templates/_shared/lib/context/task_sync.py +0 -407
  111. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  112. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  113. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  114. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  115. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  116. 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
  ]
@@ -1,8 +1,7 @@
1
1
  """Plan context templates for add_plan_context hook.
2
2
 
3
3
  Provides standardized templates for:
4
- - Evaluation context reminder
5
- - Clarifying questions offer
4
+ - Evaluation context reminder (injected on plan writes)
6
5
  """
7
6
 
8
7
 
@@ -50,39 +49,3 @@ What exactly to build/change
50
49
  - [ ] Are file paths exact (not "the auth file")?
51
50
  - [ ] Are implementation details specific (not "use the approach we discussed")?
52
51
  """.strip()
53
-
54
-
55
- def get_questions_offer_template() -> str:
56
- """Get the clarifying questions offer template.
57
-
58
- Uses persona-based questioning to surface hidden constraints.
59
-
60
- Returns:
61
- Formatted markdown prompt for offering clarifying questions
62
- """
63
- from .persona_questions import format_questions_for_prompt
64
-
65
- persona_questions = format_questions_for_prompt()
66
-
67
- return f"""
68
- ## First Plan Write - Optional Clarifying Questions
69
-
70
- Your initial plan has been saved. Before finalizing, ask the user if they'd like to answer clarifying questions to refine it.
71
-
72
- **Use AskUserQuestion now:**
73
-
74
- Header: "Questions?"
75
- Question: "I've drafted an initial plan. Would you like to answer a few clarifying questions from different perspectives so I can refine it?"
76
- Options:
77
- - "Yes, ask me questions" (description: "I'll ask targeted questions to surface hidden constraints, then update the plan")
78
- - "No, proceed as-is" (description: "Skip questions and proceed with the current plan")
79
-
80
- ### If user chooses YES:
81
-
82
- {persona_questions}
83
-
84
- After gathering answers, **update the plan file** with refined content before calling ExitPlanMode.
85
-
86
- ### If user chooses NO:
87
- Proceed directly to ExitPlanMode with the current plan.
88
- """.strip()