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
|
@@ -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
|
-
]
|
|
Binary file
|