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
|
@@ -14,6 +14,8 @@ from datetime import datetime
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Any, Dict, Optional
|
|
16
16
|
|
|
17
|
+
from .logger import log_debug, log_info, log_warn, log_error
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
def eprint(*args: Any) -> None:
|
|
19
21
|
"""Print to stderr."""
|
|
@@ -53,12 +55,12 @@ def project_dir(payload: Optional[Dict[str, Any]] = None) -> Path:
|
|
|
53
55
|
# Validate that CLAUDE_PROJECT_DIR is an absolute path
|
|
54
56
|
path = Path(p)
|
|
55
57
|
if not path.is_absolute():
|
|
56
|
-
|
|
58
|
+
log_warn("utils", "CLAUDE_PROJECT_DIR is not absolute, using cwd instead")
|
|
57
59
|
p = None
|
|
58
60
|
else:
|
|
59
61
|
# Check for suspicious patterns
|
|
60
62
|
if '..' in str(path):
|
|
61
|
-
|
|
63
|
+
log_warn("utils", "CLAUDE_PROJECT_DIR contains '..' pattern, using cwd instead")
|
|
62
64
|
p = None
|
|
63
65
|
|
|
64
66
|
if not p and payload:
|
|
@@ -151,14 +153,45 @@ def generate_context_id(summary: str, existing_ids: Optional[set] = None) -> str
|
|
|
151
153
|
# Timestamp prefix using local time, to the minute
|
|
152
154
|
timestamp = datetime.now().strftime("%y%m%d-%H%M")
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
try:
|
|
157
|
+
if not summary or not summary.strip():
|
|
158
|
+
base_id = f"{timestamp}-context"
|
|
159
|
+
else:
|
|
160
|
+
slug = None
|
|
161
|
+
|
|
162
|
+
# Tier 1: AI inference for high-quality keyword slugs
|
|
163
|
+
try:
|
|
164
|
+
from .inference import generate_context_id_slug
|
|
165
|
+
ai_slug = generate_context_id_slug(summary)
|
|
166
|
+
if ai_slug:
|
|
167
|
+
# Post-inference stop-word filter: remove generic words the AI included
|
|
168
|
+
from .stop_words import STOP_WORDS
|
|
169
|
+
filtered_words = [w for w in ai_slug.split() if w.lower() not in STOP_WORDS and len(w) > 1]
|
|
170
|
+
if len(filtered_words) >= 3:
|
|
171
|
+
slug = sanitize_title(' '.join(filtered_words), max_len=100)
|
|
172
|
+
else:
|
|
173
|
+
log_debug("utils", f"AI slug too generic after stop-word filter ({len(filtered_words)} words remain), using fallback")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
log_warn("utils", f"AI context ID slug failed, using fallback: {e}")
|
|
176
|
+
|
|
177
|
+
# Tier 2: Stop-word filtering
|
|
178
|
+
if not slug:
|
|
179
|
+
try:
|
|
180
|
+
from .stop_words import STOP_WORDS
|
|
181
|
+
words = [w for w in summary.lower().split() if w not in STOP_WORDS and len(w) > 1][:12]
|
|
182
|
+
slug = sanitize_title(' '.join(words), max_len=50)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
log_warn("utils", f"Stop-word fallback failed: {e}")
|
|
185
|
+
|
|
186
|
+
# Tier 3: Simple word-length filter (no imports needed)
|
|
187
|
+
if not slug or slug == "unknown":
|
|
188
|
+
words = [w for w in summary.lower().split() if len(w) > 2][:6]
|
|
189
|
+
slug = sanitize_title(' '.join(words), max_len=50) if words else "context"
|
|
190
|
+
|
|
191
|
+
base_id = f"{timestamp}-{slug}"
|
|
192
|
+
except Exception as e:
|
|
193
|
+
log_error("utils", f"Context ID generation failed entirely, using timestamp: {e}")
|
|
155
194
|
base_id = f"{timestamp}-context"
|
|
156
|
-
else:
|
|
157
|
-
# Use stop word filter, limit to 12 words
|
|
158
|
-
from .stop_words import STOP_WORDS
|
|
159
|
-
words = [w for w in summary.lower().split() if w not in STOP_WORDS and len(w) > 1][:12]
|
|
160
|
-
slug = sanitize_title(' '.join(words), max_len=50)
|
|
161
|
-
base_id = f"{timestamp}-{slug}"
|
|
162
195
|
|
|
163
196
|
if not existing_ids:
|
|
164
197
|
return base_id
|
|
@@ -1,110 +1,102 @@
|
|
|
1
|
-
"""Context management for AIW CLI templates.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""Context management for AIW CLI templates.
|
|
2
|
+
|
|
3
|
+
New 2-layer architecture:
|
|
4
|
+
context_store.py — CRUD for state.json + index.json
|
|
5
|
+
context_selector.py — 5-case context selection (session match, caret, plan match, default)
|
|
6
|
+
context_formatter.py — All display formatting
|
|
7
|
+
plan_manager.py — Plan archival, lookup, path extraction
|
|
8
|
+
task_tracker.py — Direct state.json task CRUD
|
|
9
|
+
"""
|
|
10
|
+
from .context_store import (
|
|
11
|
+
ContextState,
|
|
5
12
|
get_all_contexts,
|
|
6
13
|
get_context,
|
|
14
|
+
get_context_by_session_id,
|
|
7
15
|
create_context,
|
|
16
|
+
create_context_from_prompt,
|
|
8
17
|
update_context,
|
|
9
18
|
complete_context,
|
|
10
19
|
reopen_context,
|
|
11
20
|
archive_context,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Task,
|
|
18
|
-
ContextState,
|
|
19
|
-
append_event,
|
|
20
|
-
read_events,
|
|
21
|
-
get_current_state,
|
|
22
|
-
are_all_tasks_completed,
|
|
23
|
-
get_pending_tasks,
|
|
21
|
+
bind_session,
|
|
22
|
+
update_mode,
|
|
23
|
+
maybe_activate,
|
|
24
|
+
load_state,
|
|
25
|
+
save_state,
|
|
24
26
|
)
|
|
25
|
-
from .
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
verify_cache_integrity,
|
|
27
|
+
from .context_selector import (
|
|
28
|
+
determine_context,
|
|
29
|
+
BlockRequest,
|
|
30
|
+
parse_chained_caret,
|
|
31
|
+
resolve_context_by_prefix,
|
|
31
32
|
)
|
|
32
|
-
from .
|
|
33
|
-
discover_contexts_for_session,
|
|
34
|
-
get_in_flight_context,
|
|
33
|
+
from .context_formatter import (
|
|
35
34
|
format_context_list,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
format_context_created,
|
|
36
|
+
format_context_picker_stderr,
|
|
37
|
+
format_active_context_reminder,
|
|
38
|
+
format_handoff_continuation,
|
|
39
|
+
format_plan_continuation,
|
|
40
|
+
format_active_continuation,
|
|
41
|
+
format_command_feedback,
|
|
40
42
|
format_relative_time,
|
|
41
43
|
)
|
|
42
|
-
from .
|
|
44
|
+
from .plan_manager import (
|
|
45
|
+
archive_plan,
|
|
46
|
+
find_latest_plan,
|
|
47
|
+
extract_plan_path_from_result,
|
|
48
|
+
)
|
|
49
|
+
from .task_tracker import (
|
|
50
|
+
add_task,
|
|
51
|
+
update_task,
|
|
52
|
+
delete_task,
|
|
53
|
+
get_tasks,
|
|
43
54
|
generate_task_summary,
|
|
44
|
-
record_session_start,
|
|
45
|
-
record_task_created,
|
|
46
|
-
record_task_started,
|
|
47
|
-
record_task_completed,
|
|
48
|
-
record_task_blocked,
|
|
49
55
|
generate_next_task_id,
|
|
50
56
|
)
|
|
51
|
-
from .plan_archive import (
|
|
52
|
-
archive_plan_to_context,
|
|
53
|
-
)
|
|
54
|
-
from .context_extractor import (
|
|
55
|
-
extract_context_id,
|
|
56
|
-
extract_context_id_for_session,
|
|
57
|
-
)
|
|
58
57
|
|
|
59
58
|
__all__ = [
|
|
60
|
-
# Data
|
|
61
|
-
"Context",
|
|
62
|
-
"InFlightState",
|
|
63
|
-
"Task",
|
|
59
|
+
# Data model
|
|
64
60
|
"ContextState",
|
|
65
|
-
# Context
|
|
61
|
+
# Context store (CRUD)
|
|
66
62
|
"get_all_contexts",
|
|
67
63
|
"get_context",
|
|
64
|
+
"get_context_by_session_id",
|
|
68
65
|
"create_context",
|
|
66
|
+
"create_context_from_prompt",
|
|
69
67
|
"update_context",
|
|
70
68
|
"complete_context",
|
|
71
69
|
"reopen_context",
|
|
72
70
|
"archive_context",
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"rebuild_archive_index",
|
|
85
|
-
"rebuild_context_from_events",
|
|
86
|
-
"rebuild_all_caches",
|
|
87
|
-
"verify_cache_integrity",
|
|
88
|
-
# Discovery
|
|
89
|
-
"discover_contexts_for_session",
|
|
90
|
-
"get_in_flight_context",
|
|
71
|
+
"bind_session",
|
|
72
|
+
"update_mode",
|
|
73
|
+
"maybe_activate",
|
|
74
|
+
"load_state",
|
|
75
|
+
"save_state",
|
|
76
|
+
# Context selector
|
|
77
|
+
"determine_context",
|
|
78
|
+
"BlockRequest",
|
|
79
|
+
"parse_chained_caret",
|
|
80
|
+
"resolve_context_by_prefix",
|
|
81
|
+
# Formatting
|
|
91
82
|
"format_context_list",
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
83
|
+
"format_context_created",
|
|
84
|
+
"format_context_picker_stderr",
|
|
85
|
+
"format_active_context_reminder",
|
|
86
|
+
"format_handoff_continuation",
|
|
87
|
+
"format_plan_continuation",
|
|
88
|
+
"format_active_continuation",
|
|
89
|
+
"format_command_feedback",
|
|
96
90
|
"format_relative_time",
|
|
97
|
-
#
|
|
91
|
+
# Plan manager
|
|
92
|
+
"archive_plan",
|
|
93
|
+
"find_latest_plan",
|
|
94
|
+
"extract_plan_path_from_result",
|
|
95
|
+
# Task tracker
|
|
96
|
+
"add_task",
|
|
97
|
+
"update_task",
|
|
98
|
+
"delete_task",
|
|
99
|
+
"get_tasks",
|
|
98
100
|
"generate_task_summary",
|
|
99
|
-
"record_session_start",
|
|
100
|
-
"record_task_created",
|
|
101
|
-
"record_task_started",
|
|
102
|
-
"record_task_completed",
|
|
103
|
-
"record_task_blocked",
|
|
104
101
|
"generate_next_task_id",
|
|
105
|
-
# Plan Archive
|
|
106
|
-
"archive_plan_to_context",
|
|
107
|
-
# Context Extractor
|
|
108
|
-
"extract_context_id",
|
|
109
|
-
"extract_context_id_for_session",
|
|
110
102
|
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Formatting module for context display output.
|
|
2
|
+
|
|
3
|
+
Consolidates output formatting previously in discovery.py and context_enforcer.py (both now deleted).
|
|
4
|
+
All functions accept a ContextState (from context_store.py) with fields:
|
|
5
|
+
id, summary, mode, last_active, plan_path, handoff_path,
|
|
6
|
+
tasks[], last_session, session_ids, status, method, tags
|
|
7
|
+
"""
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from ..base.utils import parse_iso_timestamp
|
|
13
|
+
|
|
14
|
+
MAX_PLAN_INLINE_CHARS = 30_000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _read_plan_content(plan_path: str) -> Tuple[Optional[str], bool, int]:
|
|
18
|
+
"""Read plan content from disk for inline injection.
|
|
19
|
+
Returns (content, truncated, total_chars) or (None, False, 0) on error."""
|
|
20
|
+
try:
|
|
21
|
+
pf = Path(plan_path)
|
|
22
|
+
if not pf.exists():
|
|
23
|
+
return None, False, 0
|
|
24
|
+
content = pf.read_text(encoding="utf-8")
|
|
25
|
+
total = len(content)
|
|
26
|
+
if total > MAX_PLAN_INLINE_CHARS:
|
|
27
|
+
return content[:MAX_PLAN_INLINE_CHARS], True, total
|
|
28
|
+
return content, False, total
|
|
29
|
+
except Exception:
|
|
30
|
+
return None, False, 0
|
|
31
|
+
|
|
32
|
+
MODE_DISPLAY_MAP = {
|
|
33
|
+
"idle": "",
|
|
34
|
+
"has_plan": "[Plan Ready]",
|
|
35
|
+
"active": "[Active]",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_mode_display(mode: str) -> str:
|
|
40
|
+
"""Get bracketed display string for mode, or empty for idle."""
|
|
41
|
+
return MODE_DISPLAY_MAP.get(mode, "")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def format_relative_time(iso_timestamp: Optional[str]) -> str:
|
|
45
|
+
"""Format ISO timestamp as '2 hours ago', 'yesterday', etc."""
|
|
46
|
+
if not iso_timestamp:
|
|
47
|
+
return "unknown"
|
|
48
|
+
dt = parse_iso_timestamp(iso_timestamp)
|
|
49
|
+
if not dt:
|
|
50
|
+
return iso_timestamp[:16]
|
|
51
|
+
now = datetime.now()
|
|
52
|
+
if dt.tzinfo is not None:
|
|
53
|
+
try:
|
|
54
|
+
dt = dt.replace(tzinfo=None)
|
|
55
|
+
except Exception:
|
|
56
|
+
return iso_timestamp[:16]
|
|
57
|
+
diff = now - dt
|
|
58
|
+
if diff.days == 0:
|
|
59
|
+
hours = diff.seconds // 3600
|
|
60
|
+
if hours == 0:
|
|
61
|
+
minutes = diff.seconds // 60
|
|
62
|
+
if minutes == 0:
|
|
63
|
+
return "just now"
|
|
64
|
+
return "1 minute ago" if minutes == 1 else f"{minutes} minutes ago"
|
|
65
|
+
return "1 hour ago" if hours == 1 else f"{hours} hours ago"
|
|
66
|
+
if diff.days == 1:
|
|
67
|
+
return "yesterday"
|
|
68
|
+
if diff.days < 7:
|
|
69
|
+
return f"{diff.days} days ago"
|
|
70
|
+
return dt.strftime("%Y-%m-%d")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Internal helpers ───────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def _task_attr(task, key: str, default: str = "") -> str:
|
|
76
|
+
"""Extract attribute from a task (dict or object)."""
|
|
77
|
+
return task.get(key, default) if isinstance(task, dict) else getattr(task, key, default)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _build_restore_sections(ctx, project_root: Path = None, inline_plan: bool = False) -> str:
|
|
81
|
+
"""Build restore sections from last_session, tasks, and plan_path.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
inline_plan: If True, read and inline plan file content (for compact restore
|
|
85
|
+
where plan is NOT auto-pasted). If False, just reference the path (for
|
|
86
|
+
clear restore where Claude Code auto-pastes the plan).
|
|
87
|
+
"""
|
|
88
|
+
sections = []
|
|
89
|
+
last_session = getattr(ctx, "last_session", None) or {}
|
|
90
|
+
|
|
91
|
+
if last_session:
|
|
92
|
+
saved_at = last_session.get("saved_at", "")
|
|
93
|
+
if saved_at:
|
|
94
|
+
reason = last_session.get("save_reason", "")
|
|
95
|
+
reason_display = reason.replace("_", " ") if reason else "unknown"
|
|
96
|
+
sections.append(f"**Last session ended:** {format_relative_time(saved_at)} ({reason_display})")
|
|
97
|
+
|
|
98
|
+
tasks = getattr(ctx, "tasks", None) or []
|
|
99
|
+
if tasks:
|
|
100
|
+
buckets = {"completed": [], "in_progress": [], "pending": [], "blocked": []}
|
|
101
|
+
for t in tasks:
|
|
102
|
+
s = _task_attr(t, "status", "pending")
|
|
103
|
+
if s in buckets:
|
|
104
|
+
buckets[s].append(_task_attr(t, "subject"))
|
|
105
|
+
if any(buckets.values()):
|
|
106
|
+
sections.extend(["", f"### Previous Work ({len(tasks)} tasks)", ""])
|
|
107
|
+
marks = {"completed": "[x]", "in_progress": "[~]", "pending": "[ ]", "blocked": "[!]"}
|
|
108
|
+
for status, mark in marks.items():
|
|
109
|
+
for subj in buckets[status]:
|
|
110
|
+
sections.append(f"- {mark} {subj}")
|
|
111
|
+
|
|
112
|
+
plan_path = getattr(ctx, "plan_path", None)
|
|
113
|
+
if plan_path:
|
|
114
|
+
if inline_plan:
|
|
115
|
+
content, truncated, total_chars = _read_plan_content(plan_path)
|
|
116
|
+
if content:
|
|
117
|
+
header = f"Plan loaded from: `{plan_path}`"
|
|
118
|
+
if truncated:
|
|
119
|
+
header += f" (truncated, {total_chars} chars total)"
|
|
120
|
+
sections.extend(["", "### Plan", header, "", content])
|
|
121
|
+
if truncated:
|
|
122
|
+
sections.append(f"\n*Plan truncated at {MAX_PLAN_INLINE_CHARS} characters. Full plan at: `{plan_path}`*")
|
|
123
|
+
else:
|
|
124
|
+
sections.extend(["", "### Plan", f"*Plan file not found at `{plan_path}`.*"])
|
|
125
|
+
else:
|
|
126
|
+
sections.extend(["", "### Plan", f"Read the plan at: `{plan_path}`"])
|
|
127
|
+
|
|
128
|
+
git_state = last_session.get("git_state", {}) if last_session else {}
|
|
129
|
+
if git_state:
|
|
130
|
+
branch = git_state.get("branch", "unknown")
|
|
131
|
+
uncommitted = git_state.get("uncommitted_files", [])
|
|
132
|
+
last_commit = git_state.get("last_commit_short", "")
|
|
133
|
+
unc_str = ", ".join(uncommitted[:5]) if uncommitted else "none"
|
|
134
|
+
if len(uncommitted) > 5:
|
|
135
|
+
unc_str += f" (+{len(uncommitted) - 5} more)"
|
|
136
|
+
sections.extend(["", "### Git State", f"Branch: {branch} | Uncommitted: {unc_str}"])
|
|
137
|
+
if last_commit:
|
|
138
|
+
sections.append(f"Last commit: {last_commit}")
|
|
139
|
+
|
|
140
|
+
return "\n".join(sections)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _mode_label(ctx) -> str:
|
|
144
|
+
"""Get unbracketed mode label for inline display, defaulting to 'Active'."""
|
|
145
|
+
d = get_mode_display(getattr(ctx, "mode", "idle"))
|
|
146
|
+
return d.strip("[]") if d else "Active"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _resume_block(ctx, project_root, mode_text, instructions):
|
|
150
|
+
"""Common pattern: resume header + restore + instructions."""
|
|
151
|
+
lines = [f"## Resuming Context: {ctx.id}", "", f"**Summary:** {ctx.summary}", f"**Mode:** {mode_text}"]
|
|
152
|
+
restore = _build_restore_sections(ctx, project_root, inline_plan=True)
|
|
153
|
+
if restore:
|
|
154
|
+
lines.append(restore)
|
|
155
|
+
lines.extend(["", "---", "", "**Instructions:**"])
|
|
156
|
+
lines.extend(instructions)
|
|
157
|
+
return "\n".join(lines)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── Public formatters ──────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
def format_handoff_continuation(ctx, project_root: Path = None) -> str:
|
|
163
|
+
"""Format output when resuming a context with a pending handoff."""
|
|
164
|
+
handoff_path = getattr(ctx, "handoff_path", None) or ""
|
|
165
|
+
lines = [
|
|
166
|
+
f"## Resuming Context: {ctx.id} (Handoff Available)", "",
|
|
167
|
+
f"**Summary:** {ctx.summary}",
|
|
168
|
+
f"**Mode:** Implementing (handoff from previous session)", "",
|
|
169
|
+
]
|
|
170
|
+
try:
|
|
171
|
+
hf = Path(handoff_path)
|
|
172
|
+
if hf.exists():
|
|
173
|
+
lines.extend(["### Previous Session Handoff", "", hf.read_text(encoding="utf-8"), ""])
|
|
174
|
+
else:
|
|
175
|
+
lines.extend([f"*Handoff document not found at `{handoff_path}`*", ""])
|
|
176
|
+
except Exception as e:
|
|
177
|
+
lines.extend([f"*Handoff document at `{handoff_path}` could not be read: {e}*", ""])
|
|
178
|
+
restore = _build_restore_sections(ctx, project_root, inline_plan=True)
|
|
179
|
+
if restore:
|
|
180
|
+
lines.append(restore)
|
|
181
|
+
lines.extend(["", "---", "", "**Instructions:**",
|
|
182
|
+
"1. Review the handoff document above - especially dead ends",
|
|
183
|
+
"2. Check the plan file for remaining tasks",
|
|
184
|
+
"3. Continue implementation from where the previous session left off"])
|
|
185
|
+
return "\n".join(lines)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def format_plan_continuation(ctx, project_root: Path = None) -> str:
|
|
189
|
+
"""Format output for pending plan implementation (mode=has_plan)."""
|
|
190
|
+
return _resume_block(ctx, project_root, "Pending Implementation", [
|
|
191
|
+
"1. Review the plan and previous work above",
|
|
192
|
+
"2. Continue from where the previous session left off"])
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def format_active_continuation(ctx, project_root: Path = None) -> str:
|
|
196
|
+
"""Format output for ongoing implementation (mode=active)."""
|
|
197
|
+
return _resume_block(ctx, project_root, "Implementing", [
|
|
198
|
+
"1. Review the plan and previous work above",
|
|
199
|
+
"2. Continue from where the previous session left off"])
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def format_context_list(contexts: list) -> str:
|
|
203
|
+
"""Format list of contexts for display."""
|
|
204
|
+
if not contexts:
|
|
205
|
+
return "No active contexts found."
|
|
206
|
+
lines = ["## Active Contexts\n"]
|
|
207
|
+
for i, ctx in enumerate(contexts, 1):
|
|
208
|
+
time_str = format_relative_time(ctx.last_active)
|
|
209
|
+
md = get_mode_display(getattr(ctx, "mode", "idle"))
|
|
210
|
+
si = f" {md}" if md else ""
|
|
211
|
+
lines.append(f"**{i}. {ctx.id}**{si}")
|
|
212
|
+
lines.append(f" {ctx.summary}")
|
|
213
|
+
if ctx.method:
|
|
214
|
+
lines.append(f" Method: {ctx.method} | Last active: {time_str}")
|
|
215
|
+
else:
|
|
216
|
+
lines.append(f" Last active: {time_str}")
|
|
217
|
+
lines.append("")
|
|
218
|
+
return "\n".join(lines)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def format_context_created(ctx) -> str:
|
|
222
|
+
"""Format notification for a newly created context."""
|
|
223
|
+
return "\n".join([
|
|
224
|
+
f"## Context Created: {ctx.id}", "", f"**Summary:** {ctx.summary}", "",
|
|
225
|
+
"A new context has been created for this work.",
|
|
226
|
+
"Tasks created with TaskCreate will be persisted to this context."])
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def format_active_context_reminder(ctx, project_root: Path = None, include_restore: bool = False) -> str:
|
|
230
|
+
"""Format system reminder: lightweight (per-prompt) or rich (first-bind restore)."""
|
|
231
|
+
time_str = format_relative_time(ctx.last_active)
|
|
232
|
+
label = _mode_label(ctx)
|
|
233
|
+
if include_restore:
|
|
234
|
+
lines = [f"## Resuming Context: {ctx.id}", "", f"**Summary:** {ctx.summary}", f"**Mode:** {label}"]
|
|
235
|
+
restore = _build_restore_sections(ctx, project_root, inline_plan=True)
|
|
236
|
+
if restore:
|
|
237
|
+
lines.append(restore)
|
|
238
|
+
lines.extend(["", "---", "", "**Instructions:**",
|
|
239
|
+
"1. Review the previous work above",
|
|
240
|
+
"2. Continue from where the previous session left off"])
|
|
241
|
+
else:
|
|
242
|
+
lines = [
|
|
243
|
+
f"## Active Context: {ctx.id}", "", f"**Summary:** {ctx.summary}",
|
|
244
|
+
f"**Mode:** {label}", f"**Last Active:** {time_str}", "",
|
|
245
|
+
f'All work belongs to context "{ctx.id}".',
|
|
246
|
+
"Tasks created with TaskCreate will be persisted to this context."]
|
|
247
|
+
return "\n".join(lines)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ── Picker / command feedback ──────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
def format_context_picker_stderr(contexts: list) -> str:
|
|
253
|
+
"""Format the boxed picker shown on stderr when blocking for selection."""
|
|
254
|
+
lines = ["",
|
|
255
|
+
"+----------------------------------------------------------------+",
|
|
256
|
+
"| CONTEXT SELECTION REQUIRED |",
|
|
257
|
+
"+----------------------------------------------------------------+"]
|
|
258
|
+
selectable_count = 0
|
|
259
|
+
for i, ctx in enumerate(contexts, 1):
|
|
260
|
+
time_str = format_relative_time(ctx.last_active)
|
|
261
|
+
mode = getattr(ctx, "mode", "idle")
|
|
262
|
+
is_selectable = mode == "active" or getattr(ctx, "handoff_path", None)
|
|
263
|
+
if is_selectable:
|
|
264
|
+
selectable_count += 1
|
|
265
|
+
status = ""
|
|
266
|
+
if getattr(ctx, "handoff_path", None):
|
|
267
|
+
status = " [Handoff Ready]"
|
|
268
|
+
elif get_mode_display(mode):
|
|
269
|
+
status = f" {get_mode_display(mode)}"
|
|
270
|
+
summary = ctx.summary[:45] + "..." if len(ctx.summary) > 48 else ctx.summary
|
|
271
|
+
sel_tag = " [selectable]" if is_selectable else " [end only]"
|
|
272
|
+
lines.append(f"| ^{i} {ctx.id}{status}{sel_tag}")
|
|
273
|
+
lines.append(f"| {summary}")
|
|
274
|
+
lines.append(f"| [{time_str}]")
|
|
275
|
+
lines.append("|")
|
|
276
|
+
lines.extend([
|
|
277
|
+
"+----------------------------------------------------------------+",
|
|
278
|
+
"| Usage: |",
|
|
279
|
+
"| ^S<N> - Select context by number |",
|
|
280
|
+
"| ^E<N> - End/complete context by number |",
|
|
281
|
+
"| ^S:query - Select by ID match (race-safe) |",
|
|
282
|
+
"| ^E:query - End by ID match (race-safe) |",
|
|
283
|
+
"| ^E<N>+ - End context N and all after |",
|
|
284
|
+
"| ^E* - End ALL contexts |",
|
|
285
|
+
"| ^E1E2S3 - End #1 and #2, select #3 |",
|
|
286
|
+
"| ^E:fooS:bar - End 'foo...', select 'bar...' |",
|
|
287
|
+
"| ^0 work description - Create new context (10+ chars) |",
|
|
288
|
+
"+----------------------------------------------------------------+"])
|
|
289
|
+
if selectable_count == 0:
|
|
290
|
+
lines.extend([
|
|
291
|
+
"| NOTE: No selectable contexts. |",
|
|
292
|
+
"| Use ^E<N> to end old contexts, then ^0 to create new. |",
|
|
293
|
+
"+----------------------------------------------------------------+"])
|
|
294
|
+
lines.append("")
|
|
295
|
+
return "\n".join(lines)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def format_command_feedback(ended_contexts: list, selected_context=None) -> str:
|
|
299
|
+
"""Format feedback about caret command operations performed."""
|
|
300
|
+
lines = []
|
|
301
|
+
if ended_contexts:
|
|
302
|
+
lines.extend(["## Contexts Ended", ""])
|
|
303
|
+
for ctx in ended_contexts:
|
|
304
|
+
s = ctx.summary[:50] + "..." if len(ctx.summary) > 50 else ctx.summary
|
|
305
|
+
lines.append(f"- **{ctx.id}**: {s}")
|
|
306
|
+
lines.append("")
|
|
307
|
+
if selected_context:
|
|
308
|
+
label = _mode_label(selected_context)
|
|
309
|
+
time_str = format_relative_time(selected_context.last_active)
|
|
310
|
+
lines.extend([
|
|
311
|
+
f"## Active Context: {selected_context.id}", "",
|
|
312
|
+
f"**Summary:** {selected_context.summary}",
|
|
313
|
+
f"**Mode:** {label}", f"**Last Active:** {time_str}", "",
|
|
314
|
+
f'All work belongs to context "{selected_context.id}".',
|
|
315
|
+
"Tasks created with TaskCreate will be persisted to this context."])
|
|
316
|
+
return "\n".join(lines)
|