aiwcli 0.10.3 → 0.11.1

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 (191) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.js +28 -131
  3. package/dist/commands/init/index.js +3 -3
  4. package/dist/lib/gitignore-manager.d.ts +32 -0
  5. package/dist/lib/gitignore-manager.js +141 -2
  6. package/dist/templates/CLAUDE.md +8 -8
  7. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  8. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  9. package/dist/templates/_shared/.claude/settings.json +7 -7
  10. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  11. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  12. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  13. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  14. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  15. package/dist/templates/_shared/hooks-ts/session_end.ts +107 -0
  16. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  17. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  18. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  19. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  20. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  21. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
  22. package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
  23. package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
  24. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  25. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  26. package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
  27. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  28. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  29. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
  30. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  31. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  32. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  33. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  34. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
  35. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  36. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  37. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
  38. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  39. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  40. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  41. package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
  42. package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
  43. package/dist/templates/_shared/scripts/status_line.ts +687 -0
  44. package/dist/templates/cc-native/.claude/settings.json +175 -185
  45. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  46. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  47. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  48. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
  50. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  72. package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
  73. package/oclif.manifest.json +1 -1
  74. package/package.json +2 -2
  75. package/dist/templates/_shared/hooks/__init__.py +0 -16
  76. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  87. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  88. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  89. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  90. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  91. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  92. package/dist/templates/_shared/hooks/session_end.py +0 -173
  93. package/dist/templates/_shared/hooks/session_start.py +0 -206
  94. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  95. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  96. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  97. package/dist/templates/_shared/lib/__init__.py +0 -1
  98. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  100. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  108. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  109. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  110. package/dist/templates/_shared/lib/base/constants.py +0 -358
  111. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  112. package/dist/templates/_shared/lib/base/inference.py +0 -307
  113. package/dist/templates/_shared/lib/base/logger.py +0 -305
  114. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  115. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  116. package/dist/templates/_shared/lib/base/utils.py +0 -263
  117. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  118. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  130. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  131. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  132. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  133. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  134. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  135. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  136. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  137. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  138. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  139. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  140. package/dist/templates/_shared/lib/templates/README.md +0 -206
  141. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  142. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  145. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  146. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  147. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  148. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  149. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  150. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  151. package/dist/templates/_shared/scripts/status_line.py +0 -716
  152. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  153. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  154. package/dist/templates/cc-native/MIGRATION.md +0 -86
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  160. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  161. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  162. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  163. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  164. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  165. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  166. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  173. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  174. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  175. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  176. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  185. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  186. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  187. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  188. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  189. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  190. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  191. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -1,303 +0,0 @@
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 json
14
- import re
15
- import uuid
16
- from datetime import datetime
17
- from pathlib import Path
18
- from typing import List, Optional, Tuple
19
-
20
- from ..base.atomic_write import atomic_write
21
- from ..base.constants import get_context_dir, get_context_plans_dir
22
- from ..base.logger import log_debug, log_info, log_warn, log_error
23
- from ..base.utils import sanitize_title
24
-
25
-
26
- # ---------------------------------------------------------------------------
27
- # Plan archival
28
- # ---------------------------------------------------------------------------
29
-
30
- def archive_plan(
31
- plan_path: str,
32
- context_id: str,
33
- project_root: Path = None,
34
- ) -> Tuple[Optional[str], Optional[str], Optional[str]]:
35
- """Archive a plan file to the context's plans/ folder.
36
-
37
- Copies the plan content to:
38
- _output/contexts/{context_id}/plans/{date}-{slug}.md
39
-
40
- Computes a content hash and signature for change detection and
41
- fallback matching after /clear.
42
-
43
- Does NOT modify state.json or mode — the calling hook handles that
44
- via context_store.update_mode().
45
-
46
- Args:
47
- plan_path: Path to the source plan file.
48
- context_id: Target context identifier.
49
- project_root: Project root directory (default: from env / cwd).
50
-
51
- Returns:
52
- (archived_path, plan_hash, plan_signature) on success.
53
- (None, None, None) on any error.
54
- """
55
- plan_file = Path(plan_path)
56
- if not plan_file.exists():
57
- log_warn("plan_manager", f"Plan file not found: {plan_path}")
58
- return None, None, None
59
-
60
- # Read plan content
61
- try:
62
- content = plan_file.read_text(encoding="utf-8")
63
- except Exception as e:
64
- log_error("plan_manager", f"Failed to read plan: {e}")
65
- return None, None, None
66
-
67
- # Compute hash and signature
68
- plan_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
69
- plan_signature = content[:200]
70
-
71
- # Ensure plans directory exists
72
- plans_dir = get_context_plans_dir(context_id, project_root)
73
- plans_dir.mkdir(parents=True, exist_ok=True)
74
-
75
- # Generate archive filename: YYYY-MM-DD-<slug>.md
76
- date_str = datetime.now().strftime("%Y-%m-%d-%H%M")
77
-
78
- # Try AI inference for a descriptive slug from plan content
79
- slug = None
80
- try:
81
- from ..base.inference import generate_context_id_slug
82
- ai_slug = generate_context_id_slug(content[:500], timeout=5)
83
- if ai_slug:
84
- slug = sanitize_title(ai_slug, max_len=60)
85
- except Exception as e:
86
- log_warn("plan_manager", f"Plan slug inference failed: {e}")
87
-
88
- # Fallback: use plan filename
89
- if not slug:
90
- slug = sanitize_title(plan_file.stem, max_len=30)
91
-
92
- archive_name = f"{date_str}-{slug}.md"
93
- archive_path = plans_dir / archive_name
94
-
95
- # Handle filename collisions with counter suffix
96
- counter = 2
97
- while archive_path.exists():
98
- archive_name = f"{date_str}-{slug}-{counter}.md"
99
- archive_path = plans_dir / archive_name
100
- counter += 1
101
-
102
- # Write archived plan atomically
103
- success, error = atomic_write(archive_path, content)
104
- if not success:
105
- log_error("plan_manager", f"Failed to write archive: {error}")
106
- return None, None, None
107
-
108
- log_info("plan_manager", f"Archived plan to: {archive_path}")
109
- return str(archive_path), plan_hash, plan_signature
110
-
111
-
112
- # ---------------------------------------------------------------------------
113
- # Plan lookup
114
- # ---------------------------------------------------------------------------
115
-
116
- def find_latest_plan(
117
- context_id: str,
118
- project_root: Path = None,
119
- ) -> Optional[str]:
120
- """Find the most relevant plan file for a context.
121
-
122
- Priority:
123
- 1. state.json plan_path — if the file still exists on disk.
124
- 2. Most recent .md in plans/ directory by modification time.
125
- 3. None if no plans found.
126
-
127
- Args:
128
- context_id: Context identifier.
129
- project_root: Project root directory (default: from env / cwd).
130
-
131
- Returns:
132
- Absolute path string to the plan file, or None.
133
- """
134
- # 1. Check state.json plan_path first
135
- try:
136
- from .context_store import load_state
137
- state = load_state(context_id, project_root)
138
- if state and state.plan_path:
139
- plan_path = Path(state.plan_path)
140
- if plan_path.exists():
141
- return str(plan_path)
142
- except Exception as e:
143
- log_warn("plan_manager", f"Failed to check state.json plan_path: {e}")
144
-
145
- # 2. Fall back to most recent .md in plans/ dir by mtime
146
- plans_dir = get_context_plans_dir(context_id, project_root)
147
- if plans_dir.exists():
148
- plans = sorted(
149
- plans_dir.glob("*.md"),
150
- key=lambda p: p.stat().st_mtime,
151
- reverse=True,
152
- )
153
- if plans:
154
- return str(plans[0])
155
-
156
- # 3. No plan found
157
- return None
158
-
159
-
160
- # ---------------------------------------------------------------------------
161
- # Plan identification and normalization
162
- # ---------------------------------------------------------------------------
163
-
164
- def generate_plan_id() -> str:
165
- """Generate a short unique plan identifier (8 hex chars)."""
166
- return uuid.uuid4().hex[:8]
167
-
168
-
169
- def normalize_plan_content(text: str) -> str:
170
- """Aggressively normalize plan content for hashing.
171
-
172
- Strips all XML/HTML tags and collapses whitespace so that
173
- wrapper variations (e.g. <system-reminder>) don't affect the hash.
174
- """
175
- text = re.sub(r'<[^>]+>', '', text)
176
- text = re.sub(r'\s+', ' ', text).strip()
177
- return text
178
-
179
-
180
- def extract_plan_anchors(content: str, max_anchors: int = 5) -> List[str]:
181
- """Extract structural anchors from plan content.
182
-
183
- Returns markdown headings + first substantial paragraph as short strings.
184
- Used for fuzzy matching when hash-based matching fails.
185
- """
186
- anchors = []
187
- for line in content.splitlines():
188
- line = line.strip()
189
- if line.startswith('#') and len(line) > 3:
190
- anchors.append(line[:80])
191
- elif not anchors and len(line) > 20:
192
- anchors.append(line[:80])
193
- if len(anchors) >= max_anchors:
194
- break
195
- return anchors
196
-
197
-
198
- # ---------------------------------------------------------------------------
199
- # Transcript-based plan path extraction
200
- # ---------------------------------------------------------------------------
201
-
202
- _MAX_TRANSCRIPT_SIZE = 50 * 1024 * 1024 # 50 MB
203
-
204
- def find_plan_path_in_transcript(transcript_path: str) -> Optional[str]:
205
- """Find the plan file path by parsing the session transcript JSONL.
206
-
207
- Searches the transcript in reverse for the most recent Write tool call
208
- whose file_path targets a .claude/plans/ directory. This is deterministic:
209
- exact structural match on Path.parts, no fuzzy matching.
210
-
211
- Args:
212
- transcript_path: Absolute path to the transcript JSONL file.
213
-
214
- Returns:
215
- The file_path string from the Write tool call, or None.
216
- """
217
- if not transcript_path:
218
- return None
219
-
220
- tp = Path(transcript_path)
221
- if not tp.exists():
222
- log_debug("plan_manager", f"Transcript not found: {transcript_path}")
223
- return None
224
-
225
- try:
226
- size = tp.stat().st_size
227
- except OSError:
228
- return None
229
-
230
- if size > _MAX_TRANSCRIPT_SIZE:
231
- log_warn("plan_manager", f"Transcript too large ({size} bytes), skipping")
232
- return None
233
-
234
- try:
235
- lines = tp.read_text(encoding="utf-8").splitlines()
236
- except Exception as e:
237
- log_warn("plan_manager", f"Failed to read transcript: {e}")
238
- return None
239
-
240
- for line in reversed(lines):
241
- line = line.strip()
242
- if not line:
243
- continue
244
- try:
245
- data = json.loads(line)
246
- except (json.JSONDecodeError, ValueError):
247
- continue
248
-
249
- content = None
250
- try:
251
- content = data["message"]["content"]
252
- except (KeyError, TypeError):
253
- continue
254
-
255
- if not isinstance(content, list):
256
- continue
257
-
258
- for block in content:
259
- if not isinstance(block, dict):
260
- continue
261
- if block.get("type") != "tool_use" or block.get("name") != "Write":
262
- continue
263
- file_path = None
264
- try:
265
- file_path = block["input"]["file_path"]
266
- except (KeyError, TypeError):
267
- continue
268
- if not file_path:
269
- continue
270
-
271
- # Check if path contains .claude/plans/ as consecutive parts
272
- parts = Path(file_path).parts
273
- for i in range(len(parts) - 1):
274
- if parts[i] == ".claude" and parts[i + 1] == "plans":
275
- log_info("plan_manager", f"Extracted plan path from transcript: {file_path}")
276
- return file_path
277
-
278
- log_debug("plan_manager", "No plan Write found in transcript")
279
- return None
280
-
281
-
282
- # ---------------------------------------------------------------------------
283
- # Path extraction from tool output
284
- # ---------------------------------------------------------------------------
285
-
286
- def extract_plan_path_from_result(tool_result: str) -> Optional[str]:
287
- """Extract plan file path from ExitPlanMode tool result.
288
-
289
- Parses the pattern: "Your plan has been saved to: <path>"
290
- from the tool_result string returned by ExitPlanMode.
291
-
292
- Args:
293
- tool_result: Raw text output from the ExitPlanMode tool.
294
-
295
- Returns:
296
- Plan file path string (stripped), or None if not found.
297
- """
298
- if not tool_result:
299
- return None
300
- match = re.search(r"Your plan has been saved to:\s*(.+\.md)", tool_result)
301
- if match:
302
- return match.group(1).strip()
303
- return None
@@ -1,188 +0,0 @@
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)
@@ -1,22 +0,0 @@
1
- """Handoff utilities for context-aware session management.
2
-
3
- This module provides graceful context degradation when Claude's
4
- context window fills up. Instead of rushing or losing work,
5
- it creates a handoff document and facilitates clean session continuation.
6
-
7
- Components:
8
- - document_generator: Creates handoff documents with work state
9
- - context_monitor hook: Monitors context during tool use and triggers warnings
10
- """
11
-
12
- from .document_generator import (
13
- generate_handoff_document,
14
- get_handoff_continuation_prompt,
15
- HandoffDocument,
16
- )
17
-
18
- __all__ = [
19
- "generate_handoff_document",
20
- "get_handoff_continuation_prompt",
21
- "HandoffDocument",
22
- ]