aiwcli 0.9.8 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +3 -3
- package/dist/templates/_shared/.claude/settings.json +4 -0
- 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_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 +87 -178
- package/dist/templates/_shared/hooks/context_monitor.py +104 -247
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +47 -32
- package/dist/templates/_shared/hooks/session_end.py +103 -60
- package/dist/templates/_shared/hooks/session_start.py +110 -81
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
- package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
- package/dist/templates/_shared/lib/base/__init__.py +16 -0
- package/dist/templates/_shared/lib/base/__pycache__/__init__.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__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
- package/dist/templates/_shared/lib/base/inference.py +121 -0
- package/dist/templates/_shared/lib/base/logger.py +291 -0
- package/dist/templates/_shared/lib/base/utils.py +42 -9
- package/dist/templates/_shared/lib/context/__init__.py +72 -80
- package/dist/templates/_shared/lib/context/__pycache__/__init__.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__/plan_manager.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 +316 -0
- package/dist/templates/_shared/lib/context/context_selector.py +491 -0
- package/dist/templates/_shared/lib/context/context_store.py +636 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
- package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
- 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 +14 -40
- package/dist/templates/_shared/lib/templates/README.md +5 -13
- package/dist/templates/_shared/lib/templates/__init__.py +2 -6
- 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__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
- 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 +39 -19
- package/dist/templates/_shared/scripts/status_line.py +701 -0
- package/dist/templates/_shared/workflows/handoff.md +9 -3
- package/dist/templates/cc-native/.claude/settings.json +41 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
- package/dist/templates/cc-native/MIGRATION.md +1 -1
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
- 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 +57 -55
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
- package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.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/debug.py +37 -22
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
- 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 +26 -21
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
- package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
- package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
- package/dist/templates/_shared/lib/context/auto_state.py +0 -167
- package/dist/templates/_shared/lib/context/cache.py +0 -444
- package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
- package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
- package/dist/templates/_shared/lib/context/discovery.py +0 -554
- package/dist/templates/_shared/lib/context/event_log.py +0 -316
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -407
- package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook for TaskCreate - assesses atomicity and forkability via inference.
|
|
3
|
-
|
|
4
|
-
Ensures tasks contain sufficient self-contained context for independent execution,
|
|
5
|
-
especially when delegated to subagents with zero conversation history.
|
|
6
|
-
|
|
7
|
-
Non-blocking: Warns but allows creation even if atomicity is poor.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import sys
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
# Path setup
|
|
15
|
-
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
16
|
-
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
17
|
-
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
18
|
-
|
|
19
|
-
from lib.base.hook_utils import (
|
|
20
|
-
load_hook_input,
|
|
21
|
-
validate_hook_event,
|
|
22
|
-
get_tool_input,
|
|
23
|
-
safe_hook_main,
|
|
24
|
-
run_hook,
|
|
25
|
-
)
|
|
26
|
-
from lib.base.utils import eprint
|
|
27
|
-
from lib.base.subprocess_utils import is_internal_call
|
|
28
|
-
from lib.base.inference import inference
|
|
29
|
-
|
|
30
|
-
# Prompt engineered per Prompting/Standards.md:
|
|
31
|
-
# - Markdown-only (no XML)
|
|
32
|
-
# - Positive framing (tell what TO do)
|
|
33
|
-
# - 1-3 clear examples matching desired output
|
|
34
|
-
# - Direct imperative instructions
|
|
35
|
-
# - Explicit JSON output format
|
|
36
|
-
|
|
37
|
-
ASSESSMENT_SYSTEM_PROMPT = """Assess whether a task description is self-contained enough for a subagent with zero conversation history to execute it.
|
|
38
|
-
|
|
39
|
-
## What Makes a Good Task
|
|
40
|
-
|
|
41
|
-
A well-specified task includes:
|
|
42
|
-
- Explicit file paths: "Edit src/utils/parser.py"
|
|
43
|
-
- Specific function/component names: "Modify validate_input()"
|
|
44
|
-
- Clear expected behavior: "Return 404 when user not found"
|
|
45
|
-
- Concrete error context: "TypeError on line 45 when input is None"
|
|
46
|
-
|
|
47
|
-
## What Makes a Poor Task
|
|
48
|
-
|
|
49
|
-
Watch for context-dependent references:
|
|
50
|
-
- Dangling references: "the file above", "as discussed", "this bug"
|
|
51
|
-
- Vague actions: "fix the bug", "update it", "finish the work"
|
|
52
|
-
- Pronouns without antecedents: "it", "they", "the issue"
|
|
53
|
-
- Missing specifics: which file? what function? what behavior?
|
|
54
|
-
|
|
55
|
-
## Examples
|
|
56
|
-
|
|
57
|
-
**Atomic** — Subject: "Fix null pointer in user lookup"
|
|
58
|
-
Description: "In src/services/user.py, get_user_by_id() raises TypeError when user_id is None. Add null check at line 23 to return None instead of calling database.query()."
|
|
59
|
-
Why: file path, function, error, fix location, expected behavior.
|
|
60
|
-
|
|
61
|
-
**Not atomic** — Subject: "Fix the bug"
|
|
62
|
-
Description: "The issue we discussed earlier needs to be resolved"
|
|
63
|
-
Why: no file, no function, no error details, references conversation history.
|
|
64
|
-
|
|
65
|
-
**Partially atomic** — Subject: "Add validation to form"
|
|
66
|
-
Description: "Add email validation to the signup form. Return error if invalid."
|
|
67
|
-
Why: missing which file, what validation rules, where to display error.
|
|
68
|
-
|
|
69
|
-
## Output
|
|
70
|
-
|
|
71
|
-
Respond with JSON only:
|
|
72
|
-
{"atomic": true/false, "forkable": true/false, "issues": ["issue 1", "issue 2"], "recommendation": "actionable suggestion or 'Well-specified'"}"""
|
|
73
|
-
|
|
74
|
-
ASSESSMENT_USER_TEMPLATE = """**Subject:** {subject}
|
|
75
|
-
**Description:** {description}
|
|
76
|
-
|
|
77
|
-
Could a subagent with zero conversation history execute this task?"""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@safe_hook_main("task_create_atomicity")
|
|
81
|
-
def main() -> int:
|
|
82
|
-
# Skip internal calls (prevents recursion from orchestrator/inference)
|
|
83
|
-
if is_internal_call():
|
|
84
|
-
return 0
|
|
85
|
-
|
|
86
|
-
# Load and validate hook input
|
|
87
|
-
payload = load_hook_input()
|
|
88
|
-
if not payload:
|
|
89
|
-
return 0
|
|
90
|
-
|
|
91
|
-
# Only process TaskCreate
|
|
92
|
-
if not validate_hook_event(payload, "PreToolUse", "TaskCreate"):
|
|
93
|
-
return 0
|
|
94
|
-
|
|
95
|
-
tool_input = get_tool_input(payload)
|
|
96
|
-
if not tool_input:
|
|
97
|
-
return 0
|
|
98
|
-
|
|
99
|
-
subject = tool_input.get("subject", "")
|
|
100
|
-
description = tool_input.get("description", "")
|
|
101
|
-
|
|
102
|
-
# Skip very short tasks (likely intentionally brief or simple acknowledgments)
|
|
103
|
-
if len(description.strip()) < 15:
|
|
104
|
-
return 0
|
|
105
|
-
|
|
106
|
-
# Call inference to assess atomicity and forkability
|
|
107
|
-
result = inference(
|
|
108
|
-
system_prompt=ASSESSMENT_SYSTEM_PROMPT,
|
|
109
|
-
user_prompt=ASSESSMENT_USER_TEMPLATE.format(
|
|
110
|
-
subject=subject,
|
|
111
|
-
description=description
|
|
112
|
-
),
|
|
113
|
-
level="fast", # Use Haiku for minimal latency (~1-2s)
|
|
114
|
-
timeout=12, # Allow up to 12s for inference
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
if not result.success:
|
|
118
|
-
eprint(f"[task_create_atomicity] Inference failed: {result.error}")
|
|
119
|
-
return 0 # Non-blocking on failure
|
|
120
|
-
|
|
121
|
-
# Parse JSON response
|
|
122
|
-
try:
|
|
123
|
-
# Handle potential markdown code blocks in response
|
|
124
|
-
output = result.output.strip()
|
|
125
|
-
if output.startswith("```"):
|
|
126
|
-
# Extract JSON from code block
|
|
127
|
-
lines = output.split("\n")
|
|
128
|
-
json_lines = []
|
|
129
|
-
in_block = False
|
|
130
|
-
for line in lines:
|
|
131
|
-
if line.startswith("```") and not in_block:
|
|
132
|
-
in_block = True
|
|
133
|
-
continue
|
|
134
|
-
elif line.startswith("```") and in_block:
|
|
135
|
-
break
|
|
136
|
-
elif in_block:
|
|
137
|
-
json_lines.append(line)
|
|
138
|
-
output = "\n".join(json_lines)
|
|
139
|
-
|
|
140
|
-
assessment = json.loads(output)
|
|
141
|
-
except json.JSONDecodeError:
|
|
142
|
-
eprint(f"[task_create_atomicity] Failed to parse inference response: {result.output[:100]}")
|
|
143
|
-
return 0
|
|
144
|
-
|
|
145
|
-
# Extract assessment fields
|
|
146
|
-
atomic = assessment.get("atomic", True)
|
|
147
|
-
forkable = assessment.get("forkable", True)
|
|
148
|
-
issues = assessment.get("issues", [])
|
|
149
|
-
recommendation = assessment.get("recommendation", "")
|
|
150
|
-
|
|
151
|
-
# Build context message based on assessment
|
|
152
|
-
if atomic and forkable:
|
|
153
|
-
context_msg = "Task assessment: well-specified and ready for delegation."
|
|
154
|
-
else:
|
|
155
|
-
# Constructive guidance — what to add, not what's wrong
|
|
156
|
-
issues_text = "\n".join(f"- {issue}" for issue in issues) if issues else ""
|
|
157
|
-
|
|
158
|
-
context_msg = f"""**Enrich this task for subagent delegation**
|
|
159
|
-
|
|
160
|
-
A subagent receiving this task will have no conversation history. To make it actionable:
|
|
161
|
-
|
|
162
|
-
{issues_text}
|
|
163
|
-
|
|
164
|
-
**Suggested enrichment:** {recommendation}"""
|
|
165
|
-
|
|
166
|
-
# Output hook response with additionalContext
|
|
167
|
-
out = {
|
|
168
|
-
"hookSpecificOutput": {
|
|
169
|
-
"additionalContext": context_msg
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
print(json.dumps(out, ensure_ascii=False))
|
|
173
|
-
return 0
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if __name__ == "__main__":
|
|
177
|
-
run_hook(main)
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
"""Auto-state save/load module for session handoff.
|
|
2
|
-
|
|
3
|
-
Captures structural session state (git state, transcript path, mode)
|
|
4
|
-
for zero-friction session restoration. Does NOT capture Claude's
|
|
5
|
-
subjective state — that comes from the manual /handoff flow.
|
|
6
|
-
|
|
7
|
-
Auto-state file: _output/contexts/{context_id}/auto-state.json
|
|
8
|
-
Overwritten each save. This is a cache — if corrupted, system degrades gracefully.
|
|
9
|
-
"""
|
|
10
|
-
import json
|
|
11
|
-
import subprocess
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any, Dict, Optional
|
|
14
|
-
|
|
15
|
-
from ..base.atomic_write import atomic_write
|
|
16
|
-
from ..base.constants import get_auto_state_path, get_context_dir
|
|
17
|
-
from ..base.utils import eprint, now_iso
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
AUTO_STATE_VERSION = 1
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def capture_git_state(project_root: Path = None) -> Dict[str, Any]:
|
|
24
|
-
"""
|
|
25
|
-
Capture current git state for auto-state snapshot.
|
|
26
|
-
|
|
27
|
-
Returns:
|
|
28
|
-
Dict with branch, uncommitted_files, last_commit_short.
|
|
29
|
-
Empty dict on failure (non-git repo, git not available).
|
|
30
|
-
"""
|
|
31
|
-
if project_root is None:
|
|
32
|
-
project_root = Path.cwd()
|
|
33
|
-
|
|
34
|
-
cwd = str(project_root)
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
# Get current branch
|
|
38
|
-
branch_result = subprocess.run(
|
|
39
|
-
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
40
|
-
capture_output=True, text=True, timeout=5, cwd=cwd
|
|
41
|
-
)
|
|
42
|
-
branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "unknown"
|
|
43
|
-
|
|
44
|
-
# Get uncommitted files (modified + untracked, limited)
|
|
45
|
-
status_result = subprocess.run(
|
|
46
|
-
["git", "status", "--porcelain", "--short"],
|
|
47
|
-
capture_output=True, text=True, timeout=5, cwd=cwd
|
|
48
|
-
)
|
|
49
|
-
uncommitted = []
|
|
50
|
-
if status_result.returncode == 0:
|
|
51
|
-
for line in status_result.stdout.strip().splitlines()[:20]:
|
|
52
|
-
# Format: "XY filename" — extract just filename
|
|
53
|
-
if len(line) > 3:
|
|
54
|
-
uncommitted.append(line[3:].strip())
|
|
55
|
-
|
|
56
|
-
# Get last commit short hash + message
|
|
57
|
-
log_result = subprocess.run(
|
|
58
|
-
["git", "log", "-1", "--oneline"],
|
|
59
|
-
capture_output=True, text=True, timeout=5, cwd=cwd
|
|
60
|
-
)
|
|
61
|
-
last_commit = log_result.stdout.strip() if log_result.returncode == 0 else ""
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
"branch": branch,
|
|
65
|
-
"uncommitted_files": uncommitted,
|
|
66
|
-
"last_commit_short": last_commit,
|
|
67
|
-
}
|
|
68
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
69
|
-
eprint(f"[auto_state] Git capture failed: {e}")
|
|
70
|
-
return {}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def save_auto_state(
|
|
74
|
-
context_id: str,
|
|
75
|
-
session_id: str,
|
|
76
|
-
save_reason: str,
|
|
77
|
-
project_root: Path = None,
|
|
78
|
-
in_flight_mode: str = "none",
|
|
79
|
-
plan_path: Optional[str] = None,
|
|
80
|
-
handoff_path: Optional[str] = None,
|
|
81
|
-
transcript_path: Optional[str] = None,
|
|
82
|
-
) -> bool:
|
|
83
|
-
"""
|
|
84
|
-
Save auto-state.json for a context.
|
|
85
|
-
|
|
86
|
-
Captures structural state for session restoration. Overwrites
|
|
87
|
-
any existing auto-state file.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
context_id: Context identifier
|
|
91
|
-
session_id: Current session ID
|
|
92
|
-
save_reason: Why state was saved (session_end, pre_compact, progressive)
|
|
93
|
-
project_root: Project root directory
|
|
94
|
-
in_flight_mode: Current in-flight mode (none, planning, implementing, etc.)
|
|
95
|
-
plan_path: Path to active plan file (if any)
|
|
96
|
-
handoff_path: Path to latest handoff (if any)
|
|
97
|
-
transcript_path: Path to transcript file (if available)
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
True if saved successfully
|
|
101
|
-
"""
|
|
102
|
-
auto_state_path = get_auto_state_path(context_id, project_root)
|
|
103
|
-
|
|
104
|
-
# Ensure parent directory exists
|
|
105
|
-
auto_state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
-
|
|
107
|
-
git_state = capture_git_state(project_root)
|
|
108
|
-
|
|
109
|
-
state = {
|
|
110
|
-
"version": AUTO_STATE_VERSION,
|
|
111
|
-
"context_id": context_id,
|
|
112
|
-
"session_id": session_id,
|
|
113
|
-
"saved_at": now_iso(),
|
|
114
|
-
"save_reason": save_reason,
|
|
115
|
-
"in_flight_mode": in_flight_mode,
|
|
116
|
-
"plan_path": plan_path,
|
|
117
|
-
"latest_handoff_path": handoff_path,
|
|
118
|
-
"git_state": git_state,
|
|
119
|
-
"transcript_path": transcript_path,
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
content = json.dumps(state, indent=2, ensure_ascii=False)
|
|
124
|
-
success, error = atomic_write(auto_state_path, content)
|
|
125
|
-
if success:
|
|
126
|
-
eprint(f"[auto_state] Saved auto-state for {context_id} (reason: {save_reason})")
|
|
127
|
-
return True
|
|
128
|
-
else:
|
|
129
|
-
eprint(f"[auto_state] Failed to save: {error}")
|
|
130
|
-
return False
|
|
131
|
-
except Exception as e:
|
|
132
|
-
eprint(f"[auto_state] Error saving auto-state: {e}")
|
|
133
|
-
return False
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def load_auto_state(context_id: str, project_root: Path = None) -> Optional[Dict[str, Any]]:
|
|
137
|
-
"""
|
|
138
|
-
Load auto-state.json for a context.
|
|
139
|
-
|
|
140
|
-
Returns None if file doesn't exist or is corrupted.
|
|
141
|
-
Graceful degradation — callers should handle None.
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
context_id: Context identifier
|
|
145
|
-
project_root: Project root directory
|
|
146
|
-
|
|
147
|
-
Returns:
|
|
148
|
-
Auto-state dict or None
|
|
149
|
-
"""
|
|
150
|
-
auto_state_path = get_auto_state_path(context_id, project_root)
|
|
151
|
-
|
|
152
|
-
if not auto_state_path.exists():
|
|
153
|
-
return None
|
|
154
|
-
|
|
155
|
-
try:
|
|
156
|
-
content = auto_state_path.read_text(encoding="utf-8")
|
|
157
|
-
state = json.loads(content)
|
|
158
|
-
|
|
159
|
-
# Version check
|
|
160
|
-
if state.get("version") != AUTO_STATE_VERSION:
|
|
161
|
-
eprint(f"[auto_state] Version mismatch: expected {AUTO_STATE_VERSION}, got {state.get('version')}")
|
|
162
|
-
return None
|
|
163
|
-
|
|
164
|
-
return state
|
|
165
|
-
except (json.JSONDecodeError, UnicodeDecodeError, OSError) as e:
|
|
166
|
-
eprint(f"[auto_state] Failed to load auto-state: {e}")
|
|
167
|
-
return None
|