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,180 +0,0 @@
1
- """Cross-platform atomic file writes with security.
2
-
3
- Provides crash-safe file writes by writing to a temp file first,
4
- then atomically replacing the target. This prevents corrupted files
5
- if the process crashes mid-write.
6
-
7
- Note: This is for crash-safety, NOT for concurrent access.
8
- The shared context system assumes single-session-per-context.
9
- """
10
- import os
11
- import sys
12
- import tempfile
13
- import time
14
- from pathlib import Path
15
- from typing import Optional, Tuple
16
-
17
- if sys.platform == 'win32':
18
- import ctypes
19
- from ctypes import wintypes
20
-
21
- # Windows MoveFileEx flags
22
- MOVEFILE_REPLACE_EXISTING = 0x1
23
- MOVEFILE_WRITE_THROUGH = 0x8
24
-
25
- def _atomic_replace_windows(src: Path, dst: Path) -> None:
26
- """Atomic file replacement on Windows using MoveFileEx."""
27
- kernel32 = ctypes.windll.kernel32
28
-
29
- # Set proper function prototypes for 64-bit safety
30
- kernel32.MoveFileExW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD]
31
- kernel32.MoveFileExW.restype = wintypes.BOOL
32
-
33
- result = kernel32.MoveFileExW(
34
- str(src),
35
- str(dst),
36
- MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH
37
- )
38
- if not result:
39
- error_code = kernel32.GetLastError()
40
- raise ctypes.WinError(error_code)
41
-
42
-
43
- def atomic_write(
44
- path: Path,
45
- content: str,
46
- max_attempts: int = 2,
47
- backoff_ms: Optional[list] = None
48
- ) -> Tuple[bool, Optional[str]]:
49
- """
50
- Write file atomically with retry logic.
51
-
52
- Creates a temp file in the same directory, writes content,
53
- then atomically replaces the target file. This ensures the
54
- file is never left in a corrupted state.
55
-
56
- Args:
57
- path: Target file path
58
- content: Content to write
59
- max_attempts: Maximum retry attempts (default: 2)
60
- backoff_ms: Retry backoff in milliseconds (default: [500, 1000])
61
-
62
- Returns:
63
- Tuple of (success: bool, error_message: Optional[str])
64
- """
65
- if backoff_ms is None:
66
- backoff_ms = [500, 1000]
67
-
68
- # Ensure parent directory exists
69
- path.parent.mkdir(parents=True, exist_ok=True)
70
-
71
- for attempt in range(max_attempts):
72
- try:
73
- # Create temp file in same directory for atomic rename
74
- temp_fd, temp_path_str = tempfile.mkstemp(
75
- dir=path.parent,
76
- prefix=f".{path.stem}_",
77
- suffix=".tmp"
78
- )
79
- temp_path = Path(temp_path_str)
80
-
81
- try:
82
- # Write content to temp file
83
- with os.fdopen(temp_fd, 'w', encoding='utf-8') as f:
84
- f.write(content)
85
- f.flush()
86
- os.fsync(f.fileno()) # Force write to disk
87
-
88
- # Set restrictive permissions before rename (chmod 600)
89
- try:
90
- os.chmod(temp_path, 0o600)
91
- except OSError:
92
- pass # chmod may fail on some filesystems
93
-
94
- # Platform-specific atomic rename
95
- if sys.platform == 'win32':
96
- _atomic_replace_windows(temp_path, path)
97
- else:
98
- temp_path.replace(path) # POSIX atomic
99
-
100
- return (True, None)
101
-
102
- except Exception:
103
- # Clean up temp file on failure
104
- try:
105
- temp_path.unlink()
106
- except Exception:
107
- pass # Cleanup is best-effort
108
- raise
109
-
110
- except Exception as e:
111
- if attempt < max_attempts - 1:
112
- # Bounds-safe backoff indexing
113
- wait_ms = backoff_ms[min(attempt, len(backoff_ms) - 1)]
114
- time.sleep(wait_ms / 1000.0)
115
- else:
116
- # Sanitize error message (no paths, no stack trace)
117
- error_type = type(e).__name__
118
- error_msg = str(e).split('\n')[0][:200] # First line only, max 200 chars
119
- return (False, f"{error_type}: {error_msg}")
120
-
121
- return (False, "Max retry attempts exceeded")
122
-
123
-
124
- def atomic_append(
125
- path: Path,
126
- content: str,
127
- max_attempts: int = 2,
128
- backoff_ms: Optional[list] = None
129
- ) -> Tuple[bool, Optional[str]]:
130
- """
131
- Append to file atomically with retry logic.
132
-
133
- For JSONL files, this is safe because each line is independent.
134
- If process crashes mid-append, only the last partial line is lost,
135
- which read_events() handles gracefully.
136
-
137
- Args:
138
- path: Target file path
139
- content: Content to append (should include newline if needed)
140
- max_attempts: Maximum retry attempts (default: 2)
141
- backoff_ms: Retry backoff in milliseconds (default: [500, 1000])
142
-
143
- Returns:
144
- Tuple of (success: bool, error_message: Optional[str])
145
- """
146
- if backoff_ms is None:
147
- backoff_ms = [500, 1000]
148
-
149
- # Ensure parent directory exists
150
- path.parent.mkdir(parents=True, exist_ok=True)
151
-
152
- # Check if file is being created (for permission setting)
153
- is_new_file = not path.exists()
154
-
155
- for attempt in range(max_attempts):
156
- try:
157
- with open(path, 'a', encoding='utf-8') as f:
158
- f.write(content)
159
- f.flush()
160
- os.fsync(f.fileno()) # Force write to disk
161
-
162
- # Set restrictive permissions on newly created files (chmod 600)
163
- if is_new_file:
164
- try:
165
- os.chmod(path, 0o600)
166
- except OSError:
167
- pass # chmod may fail on some filesystems
168
-
169
- return (True, None)
170
-
171
- except Exception as e:
172
- if attempt < max_attempts - 1:
173
- wait_ms = backoff_ms[min(attempt, len(backoff_ms) - 1)]
174
- time.sleep(wait_ms / 1000.0)
175
- else:
176
- error_type = type(e).__name__
177
- error_msg = str(e).split('\n')[0][:200]
178
- return (False, f"{error_type}: {error_msg}")
179
-
180
- return (False, "Max retry attempts exceeded")
@@ -1,358 +0,0 @@
1
- """Constants and path utilities for shared context management.
2
-
3
- Data hierarchy:
4
- events.jsonl (source of truth)
5
- → context.json (L1 cache)
6
- → index.json (L2 cache)
7
-
8
- All data written to _output/contexts/ (method-agnostic).
9
- No method subfolders - method is just metadata on the context.
10
- """
11
- import os
12
- import re
13
- from pathlib import Path
14
-
15
- # Directory names (relative to project root)
16
- OUTPUT_DIR = "_output"
17
- CONTEXTS_DIR = "contexts"
18
- ARCHIVE_DIR = "_archive"
19
- INDEX_FILENAME = "index.json"
20
-
21
- # Context ID validation
22
- MAX_CONTEXT_ID_LENGTH = 64
23
- VALID_CONTEXT_ID_PATTERN = re.compile(r'^[a-z0-9][a-z0-9_-]*[a-z0-9]$|^[a-z0-9]$')
24
-
25
- # File size limits
26
- MAX_EVENT_SIZE = 64 * 1024 # 64KB per event (reasonable limit)
27
- MAX_INDEX_SIZE = 1024 * 1024 # 1MB for index.json
28
-
29
- # Performance constants
30
- MAX_RETRY_ATTEMPTS = 2
31
- RETRY_BACKOFF_MS = [500, 1000]
32
-
33
-
34
- def sanitize_context_id(context_id: str) -> str:
35
- """
36
- Sanitize a string into a valid context ID.
37
-
38
- Performs these transformations:
39
- - Convert to lowercase
40
- - Replace invalid characters with hyphens
41
- - Collapse consecutive hyphens/underscores
42
- - Strip leading/trailing non-alphanumeric
43
- - Truncate to MAX_CONTEXT_ID_LENGTH
44
-
45
- Args:
46
- context_id: Raw input string
47
-
48
- Returns:
49
- Valid context ID string, or "context" if input is empty/invalid
50
- """
51
- if not context_id:
52
- return "context"
53
-
54
- # Normalize to lowercase
55
- result = context_id.lower()
56
-
57
- # Replace any character that's not alphanumeric, hyphen, or underscore
58
- result = re.sub(r'[^a-z0-9_-]', '-', result)
59
-
60
- # Collapse consecutive hyphens/underscores into single hyphen
61
- result = re.sub(r'[-_]+', '-', result)
62
-
63
- # Strip leading/trailing non-alphanumeric
64
- result = result.strip('-_')
65
-
66
- # Truncate to max length
67
- if len(result) > MAX_CONTEXT_ID_LENGTH:
68
- result = result[:MAX_CONTEXT_ID_LENGTH].rstrip('-_')
69
-
70
- # If nothing left, return default
71
- return result if result else "context"
72
-
73
-
74
- def validate_context_id(context_id: str) -> str:
75
- """
76
- Validate and normalize context ID.
77
-
78
- Auto-sanitizes invalid input instead of throwing errors for format issues.
79
- Only throws for security violations (path traversal).
80
-
81
- Valid context IDs:
82
- - 1-64 characters
83
- - lowercase alphanumeric, hyphens, underscores
84
- - must start and end with alphanumeric
85
- - no consecutive hyphens/underscores
86
-
87
- Raises:
88
- ValueError: Only for path traversal attempts
89
- """
90
- if not context_id:
91
- return "context"
92
-
93
- # SECURITY: Check for path traversal BEFORE any normalization
94
- # This prevents encoded or case-variant attacks
95
- if '..' in context_id or '/' in context_id or '\\' in context_id:
96
- raise ValueError(f"Invalid context ID '{context_id}': path traversal not allowed")
97
-
98
- # Also check for URL-encoded variants
99
- if '%2e' in context_id.lower() or '%2f' in context_id.lower() or '%5c' in context_id.lower():
100
- raise ValueError(f"Invalid context ID '{context_id}': encoded path traversal not allowed")
101
-
102
- # Sanitize instead of throwing for format issues
103
- sanitized = sanitize_context_id(context_id)
104
-
105
- return sanitized
106
-
107
-
108
- def get_output_dir(project_root: Path = None) -> Path:
109
- """
110
- Get the output directory path.
111
-
112
- Args:
113
- project_root: Project root directory (default: cwd)
114
-
115
- Returns:
116
- Path to _output/
117
- """
118
- if project_root is None:
119
- project_root = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
120
- return Path(project_root) / OUTPUT_DIR
121
-
122
-
123
- def get_contexts_dir(project_root: Path = None) -> Path:
124
- """
125
- Get the contexts directory path.
126
-
127
- Args:
128
- project_root: Project root directory (default: cwd)
129
-
130
- Returns:
131
- Path to _output/contexts/
132
- """
133
- return get_output_dir(project_root) / CONTEXTS_DIR
134
-
135
-
136
- def get_context_dir(context_id: str, project_root: Path = None) -> Path:
137
- """
138
- Get the directory path for a specific context.
139
-
140
- Args:
141
- context_id: Context identifier
142
- project_root: Project root directory (default: cwd)
143
-
144
- Returns:
145
- Path to _output/contexts/{context_id}/
146
-
147
- Raises:
148
- ValueError: If context_id is invalid or path escapes expected directory
149
- """
150
- validated_id = validate_context_id(context_id)
151
- contexts_dir = get_contexts_dir(project_root)
152
- result_path = contexts_dir / validated_id
153
-
154
- # SECURITY: Verify resolved path stays within contexts directory
155
- # This prevents symlink attacks and any path manipulation we might have missed
156
- try:
157
- resolved = result_path.resolve()
158
- contexts_resolved = contexts_dir.resolve()
159
- # Check that resolved path starts with the contexts directory
160
- # Use os.path for cross-platform compatibility
161
- import os
162
- resolved_str = os.path.normcase(str(resolved))
163
- contexts_str = os.path.normcase(str(contexts_resolved))
164
- if not resolved_str.startswith(contexts_str):
165
- raise ValueError(f"Invalid context ID '{context_id}': path escapes contexts directory")
166
- except (OSError, ValueError) as e:
167
- if isinstance(e, ValueError):
168
- raise
169
- # OSError can occur if path doesn't exist yet, which is fine for creation
170
- pass
171
-
172
- return result_path
173
-
174
-
175
- def get_context_plans_dir(context_id: str, project_root: Path = None) -> Path:
176
- """
177
- Get the plans directory for a specific context.
178
-
179
- Args:
180
- context_id: Context identifier
181
- project_root: Project root directory (default: cwd)
182
-
183
- Returns:
184
- Path to _output/contexts/{context_id}/plans/
185
- """
186
- return get_context_dir(context_id, project_root) / "plans"
187
-
188
-
189
- def get_context_handoffs_dir(context_id: str, project_root: Path = None) -> Path:
190
- """
191
- Get the handoffs directory for a specific context.
192
-
193
- Args:
194
- context_id: Context identifier
195
- project_root: Project root directory (default: cwd)
196
-
197
- Returns:
198
- Path to _output/contexts/{context_id}/handoffs/
199
- """
200
- return get_context_dir(context_id, project_root) / "handoffs"
201
-
202
-
203
- def get_context_reviews_dir(context_id: str, project_root: Path = None) -> Path:
204
- """
205
- Get the reviews directory for a specific context.
206
-
207
- Args:
208
- context_id: Context identifier
209
- project_root: Project root directory (default: cwd)
210
-
211
- Returns:
212
- Path to _output/contexts/{context_id}/reviews/
213
- """
214
- return get_context_dir(context_id, project_root) / "reviews"
215
-
216
-
217
- def get_index_path(project_root: Path = None) -> Path:
218
- """
219
- Get the global index file path.
220
-
221
- Args:
222
- project_root: Project root directory (default: cwd)
223
-
224
- Returns:
225
- Path to _output/index.json
226
- """
227
- return get_output_dir(project_root) / INDEX_FILENAME
228
-
229
-
230
- def get_context_file_path(context_id: str, project_root: Path = None) -> Path:
231
- """
232
- Get the context.json file path for a specific context.
233
-
234
- Args:
235
- context_id: Context identifier
236
- project_root: Project root directory (default: cwd)
237
-
238
- Returns:
239
- Path to _output/contexts/{context_id}/context.json
240
- """
241
- return get_context_dir(context_id, project_root) / "context.json"
242
-
243
-
244
- def get_events_file_path(context_id: str, project_root: Path = None) -> Path:
245
- """
246
- Get the events.jsonl file path for a specific context.
247
-
248
- Args:
249
- context_id: Context identifier
250
- project_root: Project root directory (default: cwd)
251
-
252
- Returns:
253
- Path to _output/contexts/{context_id}/events.jsonl
254
- """
255
- return get_context_dir(context_id, project_root) / "events.jsonl"
256
-
257
-
258
- def get_auto_state_path(context_id: str, project_root: Path = None) -> Path:
259
- """
260
- Get the auto-state.json file path for a specific context.
261
-
262
- Args:
263
- context_id: Context identifier
264
- project_root: Project root directory (default: cwd)
265
-
266
- Returns:
267
- Path to _output/contexts/{context_id}/auto-state.json
268
- """
269
- return get_context_dir(context_id, project_root) / "auto-state.json"
270
-
271
-
272
- def get_archive_dir(project_root: Path = None) -> Path:
273
- """
274
- Get the archive directory path.
275
-
276
- Args:
277
- project_root: Project root directory (default: cwd)
278
-
279
- Returns:
280
- Path to _output/contexts/_archive/
281
- """
282
- return get_contexts_dir(project_root) / ARCHIVE_DIR
283
-
284
-
285
- def get_archive_context_dir(context_id: str, project_root: Path = None) -> Path:
286
- """
287
- Get the archive directory for a specific context.
288
-
289
- Args:
290
- context_id: Context identifier
291
- project_root: Project root directory (default: cwd)
292
-
293
- Returns:
294
- Path to _output/contexts/_archive/{context_id}/
295
-
296
- Raises:
297
- ValueError: If context_id is invalid
298
- """
299
- validated_id = validate_context_id(context_id)
300
- return get_archive_dir(project_root) / validated_id
301
-
302
-
303
- def get_archive_index_path(project_root: Path = None) -> Path:
304
- """
305
- Get the archive index file path.
306
-
307
- Args:
308
- project_root: Project root directory (default: cwd)
309
-
310
- Returns:
311
- Path to _output/contexts/_archive/index.json
312
- """
313
- return get_archive_dir(project_root) / INDEX_FILENAME
314
-
315
-
316
- def get_handoff_folder_path(context_id: str, project_root: Path = None) -> Path:
317
- """Get path for a new handoff folder with datetime naming.
318
-
319
- Returns: _output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/
320
- Handles collisions by appending -N suffix if folder exists.
321
-
322
- Args:
323
- context_id: Context identifier
324
- project_root: Project root directory (default: cwd)
325
-
326
- Returns:
327
- Path to new handoff folder (not yet created)
328
- """
329
- from datetime import datetime
330
- handoffs_dir = get_context_handoffs_dir(context_id, project_root)
331
- timestamp = datetime.now().strftime("%Y-%m-%d-%H%M")
332
- folder = handoffs_dir / timestamp
333
-
334
- counter = 1
335
- while folder.exists():
336
- folder = handoffs_dir / f"{timestamp}-{counter}"
337
- counter += 1
338
-
339
- return folder
340
-
341
-
342
- def get_review_folder_path(context_id: str, iteration: int, project_root: Path = None) -> Path:
343
- """Get path for a new review folder with datetime and iteration naming.
344
-
345
- Returns: _output/contexts/{context_id}/reviews/cc-native/{YYYY-MM-DD-HHMM-iteration-N}/
346
-
347
- Args:
348
- context_id: Context identifier
349
- iteration: Iteration number (1-based)
350
- project_root: Project root directory (default: cwd)
351
-
352
- Returns:
353
- Path to new review folder (not yet created)
354
- """
355
- from datetime import datetime
356
- reviews_dir = get_context_reviews_dir(context_id, project_root) / "cc-native"
357
- timestamp = datetime.now().strftime("%Y-%m-%d-%H%M")
358
- return reviews_dir / f"{timestamp}-iteration-{iteration}"