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.
- package/bin/run.js +1 -1
- package/dist/commands/clear.js +28 -131
- package/dist/commands/init/index.js +3 -3
- package/dist/lib/gitignore-manager.d.ts +32 -0
- package/dist/lib/gitignore-manager.js +141 -2
- package/dist/templates/CLAUDE.md +8 -8
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
- package/dist/templates/_shared/.claude/settings.json +7 -7
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
- package/dist/templates/_shared/hooks-ts/session_end.ts +107 -0
- package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
- package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
- package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
- package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
- package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
- package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
- package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
- package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
- package/dist/templates/_shared/lib-ts/types.ts +68 -55
- package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
- package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
- package/dist/templates/_shared/scripts/status_line.ts +687 -0
- package/dist/templates/cc-native/.claude/settings.json +175 -185
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
- package/dist/templates/_shared/hooks/__init__.py +0 -16
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +0 -177
- package/dist/templates/_shared/hooks/context_monitor.py +0 -270
- package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
- package/dist/templates/_shared/hooks/pre_compact.py +0 -104
- package/dist/templates/_shared/hooks/session_end.py +0 -173
- package/dist/templates/_shared/hooks/session_start.py +0 -206
- package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
- package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
- package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
- package/dist/templates/_shared/lib/__init__.py +0 -1
- package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__init__.py +0 -65
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
- package/dist/templates/_shared/lib/base/constants.py +0 -358
- package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
- package/dist/templates/_shared/lib/base/inference.py +0 -307
- package/dist/templates/_shared/lib/base/logger.py +0 -305
- package/dist/templates/_shared/lib/base/stop_words.py +0 -221
- package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
- package/dist/templates/_shared/lib/base/utils.py +0 -263
- package/dist/templates/_shared/lib/context/__init__.py +0 -102
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
- package/dist/templates/_shared/lib/context/context_selector.py +0 -508
- package/dist/templates/_shared/lib/context/context_store.py +0 -653
- package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
- package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
- package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
- package/dist/templates/_shared/lib/templates/README.md +0 -206
- package/dist/templates/_shared/lib/templates/__init__.py +0 -36
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/formatters.py +0 -146
- package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +0 -357
- package/dist/templates/_shared/scripts/status_line.py +0 -716
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/MIGRATION.md +0 -86
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
- package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
- package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
- package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
|
Binary file
|
|
@@ -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}"
|