aiwcli 0.9.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/README.md +1248 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +16 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +19 -0
- package/dist/commands/branch.d.ts +45 -0
- package/dist/commands/branch.js +488 -0
- package/dist/commands/clean.d.ts +34 -0
- package/dist/commands/clean.js +186 -0
- package/dist/commands/clear.d.ts +51 -0
- package/dist/commands/clear.js +835 -0
- package/dist/commands/init/index.d.ts +107 -0
- package/dist/commands/init/index.js +565 -0
- package/dist/commands/launch.d.ts +21 -0
- package/dist/commands/launch.js +108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/base-command.d.ts +114 -0
- package/dist/lib/base-command.js +153 -0
- package/dist/lib/bmad-installer.d.ts +38 -0
- package/dist/lib/bmad-installer.js +145 -0
- package/dist/lib/claude-settings-types.d.ts +102 -0
- package/dist/lib/claude-settings-types.js +5 -0
- package/dist/lib/config.d.ts +25 -0
- package/dist/lib/config.js +46 -0
- package/dist/lib/debug.d.ts +39 -0
- package/dist/lib/debug.js +74 -0
- package/dist/lib/env-compat.d.ts +26 -0
- package/dist/lib/env-compat.js +35 -0
- package/dist/lib/errors.d.ts +126 -0
- package/dist/lib/errors.js +145 -0
- package/dist/lib/generic-merge.d.ts +74 -0
- package/dist/lib/generic-merge.js +105 -0
- package/dist/lib/git/branch.d.ts +67 -0
- package/dist/lib/git/branch.js +155 -0
- package/dist/lib/git/index.d.ts +11 -0
- package/dist/lib/git/index.js +13 -0
- package/dist/lib/git/safety-checks.d.ts +44 -0
- package/dist/lib/git/safety-checks.js +102 -0
- package/dist/lib/git/types.d.ts +31 -0
- package/dist/lib/git/types.js +6 -0
- package/dist/lib/git/worktree.d.ts +67 -0
- package/dist/lib/git/worktree.js +220 -0
- package/dist/lib/gitignore-manager.d.ts +10 -0
- package/dist/lib/gitignore-manager.js +60 -0
- package/dist/lib/hooks-merger.d.ts +28 -0
- package/dist/lib/hooks-merger.js +94 -0
- package/dist/lib/ide-path-resolver.d.ts +102 -0
- package/dist/lib/ide-path-resolver.js +129 -0
- package/dist/lib/index.d.ts +13 -0
- package/dist/lib/index.js +22 -0
- package/dist/lib/output.d.ts +51 -0
- package/dist/lib/output.js +76 -0
- package/dist/lib/paths.d.ts +66 -0
- package/dist/lib/paths.js +136 -0
- package/dist/lib/quiet.d.ts +12 -0
- package/dist/lib/quiet.js +17 -0
- package/dist/lib/settings-hierarchy.d.ts +42 -0
- package/dist/lib/settings-hierarchy.js +105 -0
- package/dist/lib/spawn.d.ts +105 -0
- package/dist/lib/spawn.js +157 -0
- package/dist/lib/spinner.d.ts +19 -0
- package/dist/lib/spinner.js +34 -0
- package/dist/lib/stdin.d.ts +48 -0
- package/dist/lib/stdin.js +60 -0
- package/dist/lib/template-installer.d.ts +92 -0
- package/dist/lib/template-installer.js +375 -0
- package/dist/lib/template-linter.d.ts +49 -0
- package/dist/lib/template-linter.js +173 -0
- package/dist/lib/template-merger.d.ts +47 -0
- package/dist/lib/template-merger.js +173 -0
- package/dist/lib/template-resolver.d.ts +20 -0
- package/dist/lib/template-resolver.js +60 -0
- package/dist/lib/terminal.d.ts +102 -0
- package/dist/lib/terminal.js +245 -0
- package/dist/lib/tty-detection.d.ts +62 -0
- package/dist/lib/tty-detection.js +83 -0
- package/dist/lib/user-utils.d.ts +5 -0
- package/dist/lib/user-utils.js +23 -0
- package/dist/lib/version.d.ts +99 -0
- package/dist/lib/version.js +144 -0
- package/dist/lib/watch-templates.d.ts +6 -0
- package/dist/lib/watch-templates.js +73 -0
- package/dist/lib/windsurf-hooks-hierarchy.d.ts +30 -0
- package/dist/lib/windsurf-hooks-hierarchy.js +66 -0
- package/dist/lib/windsurf-hooks-merger.d.ts +26 -0
- package/dist/lib/windsurf-hooks-merger.js +53 -0
- package/dist/lib/windsurf-hooks-types.d.ts +33 -0
- package/dist/lib/windsurf-hooks-types.js +5 -0
- package/dist/templates/CLAUDE.md +174 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +14 -0
- package/dist/templates/_shared/.claude/settings.json +61 -0
- package/dist/templates/_shared/.codex/workflows/handoff.md +14 -0
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +14 -0
- package/dist/templates/_shared/hooks/__init__.py +16 -0
- package/dist/templates/_shared/hooks/archive_plan.py +270 -0
- package/dist/templates/_shared/hooks/context_enforcer.py +621 -0
- package/dist/templates/_shared/hooks/context_monitor.py +322 -0
- package/dist/templates/_shared/hooks/file-suggestion.py +188 -0
- package/dist/templates/_shared/hooks/task_create_capture.py +194 -0
- package/dist/templates/_shared/hooks/task_update_capture.py +254 -0
- package/dist/templates/_shared/hooks/user_prompt_submit.py +157 -0
- package/dist/templates/_shared/lib/__init__.py +1 -0
- package/dist/templates/_shared/lib/base/__init__.py +49 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +180 -0
- package/dist/templates/_shared/lib/base/constants.py +299 -0
- package/dist/templates/_shared/lib/base/inference.py +189 -0
- package/dist/templates/_shared/lib/base/utils.py +216 -0
- package/dist/templates/_shared/lib/context/__init__.py +119 -0
- 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_manager.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/cache.py +446 -0
- package/dist/templates/_shared/lib/context/context_manager.py +1171 -0
- package/dist/templates/_shared/lib/context/discovery.py +486 -0
- package/dist/templates/_shared/lib/context/event_log.py +308 -0
- package/dist/templates/_shared/lib/context/plan_archive.py +247 -0
- package/dist/templates/_shared/lib/context/task_sync.py +367 -0
- package/dist/templates/_shared/lib/handoff/__init__.py +22 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +307 -0
- package/dist/templates/_shared/lib/templates/README.md +215 -0
- package/dist/templates/_shared/lib/templates/__init__.py +40 -0
- package/dist/templates/_shared/lib/templates/formatters.py +147 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +119 -0
- package/dist/templates/_shared/scripts/save_handoff.py +99 -0
- package/dist/templates/_shared/workflows/handoff.md +212 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +80 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +75 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +239 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +109 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +71 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +104 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +93 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +223 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/DOCUMENTATION-REVIEWER.md +73 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +93 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +103 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +145 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +248 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +235 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +80 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +76 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +141 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +240 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +211 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +101 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +197 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +97 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +349 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +106 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +205 -0
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +8 -0
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -0
- package/dist/templates/cc-native/.claude/settings.json +119 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -0
- package/dist/templates/cc-native/CC-NATIVE-README.md +192 -0
- package/dist/templates/cc-native/MIGRATION.md +86 -0
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +331 -0
- package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +147 -0
- 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__/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__/cc-native-plan-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/hooks/add_plan_context.py +150 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +746 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +339 -0
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +57 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.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/async_archive.py +68 -0
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +98 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +45 -0
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +273 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +28 -0
- 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 +164 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +89 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +119 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +103 -0
- package/dist/templates/cc-native/_cc-native/lib/state.py +251 -0
- package/dist/templates/cc-native/_cc-native/lib/utils.py +830 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +76 -0
- 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 +151 -0
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +134 -0
- package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -0
- package/dist/types/exit-codes.d.ts +11 -0
- package/dist/types/exit-codes.js +10 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +7 -0
- package/oclif.manifest.json +405 -0
- package/package.json +109 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Event log utilities for context management.
|
|
2
|
+
|
|
3
|
+
events.jsonl is the SOURCE OF TRUTH for each context.
|
|
4
|
+
All state is derived by replaying these events.
|
|
5
|
+
|
|
6
|
+
Event format (one JSON object per line):
|
|
7
|
+
{"event": "event_type", "timestamp": "ISO8601", ...event-specific fields}
|
|
8
|
+
|
|
9
|
+
Crash safety:
|
|
10
|
+
- Append-only file
|
|
11
|
+
- Each line is independent JSON
|
|
12
|
+
- Corrupted lines are skipped with warning
|
|
13
|
+
- Valid events before corruption are preserved
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
from ..base.atomic_write import atomic_append
|
|
22
|
+
from ..base.constants import get_events_file_path
|
|
23
|
+
from ..base.utils import eprint, now_iso
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Event type constants
|
|
27
|
+
EVENT_CONTEXT_CREATED = "context_created"
|
|
28
|
+
EVENT_CONTEXT_COMPLETED = "context_completed"
|
|
29
|
+
EVENT_CONTEXT_REOPENED = "context_reopened"
|
|
30
|
+
EVENT_CONTEXT_ARCHIVED = "context_archived"
|
|
31
|
+
EVENT_METADATA_UPDATED = "metadata_updated"
|
|
32
|
+
EVENT_TASK_ADDED = "task_added"
|
|
33
|
+
EVENT_TASK_STARTED = "task_started"
|
|
34
|
+
EVENT_TASK_COMPLETED = "task_completed"
|
|
35
|
+
EVENT_TASK_BLOCKED = "task_blocked"
|
|
36
|
+
EVENT_NOTE_ADDED = "note_added"
|
|
37
|
+
EVENT_SESSION_STARTED = "session_started"
|
|
38
|
+
EVENT_PLANNING_STARTED = "planning_started"
|
|
39
|
+
EVENT_PLAN_CREATED = "plan_created"
|
|
40
|
+
EVENT_PLAN_IMPLEMENTATION_STARTED = "plan_implementation_started"
|
|
41
|
+
EVENT_PLAN_COMPLETED = "plan_completed"
|
|
42
|
+
EVENT_HANDOFF_CREATED = "handoff_created"
|
|
43
|
+
EVENT_HANDOFF_CLEARED = "handoff_cleared"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Task:
|
|
48
|
+
"""Task state derived from events."""
|
|
49
|
+
id: str
|
|
50
|
+
subject: str
|
|
51
|
+
description: str = ""
|
|
52
|
+
active_form: str = ""
|
|
53
|
+
status: str = "pending" # pending, in_progress, completed, blocked
|
|
54
|
+
evidence: str = ""
|
|
55
|
+
work_summary: str = ""
|
|
56
|
+
files_changed: List[str] = field(default_factory=list)
|
|
57
|
+
blocked_reason: str = ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ContextState:
|
|
62
|
+
"""Current state of a context, derived from events."""
|
|
63
|
+
id: str
|
|
64
|
+
status: str = "active" # active, completed
|
|
65
|
+
summary: str = ""
|
|
66
|
+
method: Optional[str] = None
|
|
67
|
+
tags: List[str] = field(default_factory=list)
|
|
68
|
+
created_at: Optional[str] = None
|
|
69
|
+
last_active: Optional[str] = None
|
|
70
|
+
tasks: List[Task] = field(default_factory=list)
|
|
71
|
+
notes: List[str] = field(default_factory=list)
|
|
72
|
+
plan_status: str = "none" # none, planning, pending_implementation, implementing
|
|
73
|
+
plan_path: Optional[str] = None
|
|
74
|
+
plan_hash: Optional[str] = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def read_events(context_id: str, project_root: Path = None) -> List[Dict[str, Any]]:
|
|
78
|
+
"""
|
|
79
|
+
Read all events from a context's events.jsonl file.
|
|
80
|
+
|
|
81
|
+
Handles corrupted lines gracefully by skipping them with a warning.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
context_id: Context identifier
|
|
85
|
+
project_root: Project root directory (default: cwd)
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of event dictionaries, in chronological order
|
|
89
|
+
"""
|
|
90
|
+
events_path = get_events_file_path(context_id, project_root)
|
|
91
|
+
|
|
92
|
+
if not events_path.exists():
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
events = []
|
|
96
|
+
try:
|
|
97
|
+
content = events_path.read_text(encoding='utf-8')
|
|
98
|
+
for line_num, line in enumerate(content.splitlines(), 1):
|
|
99
|
+
line = line.strip()
|
|
100
|
+
if not line:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
event = json.loads(line)
|
|
105
|
+
events.append(event)
|
|
106
|
+
except json.JSONDecodeError:
|
|
107
|
+
eprint(f"[event_log] WARNING: Skipping corrupted line {line_num} in {events_path}")
|
|
108
|
+
|
|
109
|
+
except UnicodeDecodeError as e:
|
|
110
|
+
eprint(f"[event_log] WARNING: Invalid UTF-8 in events file {events_path}, attempting fallback read")
|
|
111
|
+
# Try reading with error handling to salvage what we can
|
|
112
|
+
try:
|
|
113
|
+
content = events_path.read_text(encoding='utf-8', errors='replace')
|
|
114
|
+
for line_num, line in enumerate(content.splitlines(), 1):
|
|
115
|
+
line = line.strip()
|
|
116
|
+
if not line:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
event = json.loads(line)
|
|
121
|
+
events.append(event)
|
|
122
|
+
except json.JSONDecodeError:
|
|
123
|
+
eprint(f"[event_log] WARNING: Skipping corrupted line {line_num} in {events_path}")
|
|
124
|
+
except Exception as fallback_error:
|
|
125
|
+
eprint(f"[event_log] ERROR: Fallback read failed: {fallback_error}")
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
eprint(f"[event_log] ERROR reading events file: {e}")
|
|
129
|
+
|
|
130
|
+
return events
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def append_event(
|
|
134
|
+
context_id: str,
|
|
135
|
+
event_type: str,
|
|
136
|
+
project_root: Path = None,
|
|
137
|
+
**event_data
|
|
138
|
+
) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
Append an event to a context's events.jsonl file.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
context_id: Context identifier
|
|
144
|
+
event_type: Type of event (e.g., "task_added", "context_completed")
|
|
145
|
+
project_root: Project root directory (default: cwd)
|
|
146
|
+
**event_data: Additional event-specific data
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
True if event was successfully appended
|
|
150
|
+
"""
|
|
151
|
+
events_path = get_events_file_path(context_id, project_root)
|
|
152
|
+
|
|
153
|
+
event = {
|
|
154
|
+
"event": event_type,
|
|
155
|
+
"timestamp": now_iso(),
|
|
156
|
+
**event_data
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
event_json = json.dumps(event, ensure_ascii=False)
|
|
161
|
+
success, error = atomic_append(events_path, event_json + "\n")
|
|
162
|
+
|
|
163
|
+
if not success:
|
|
164
|
+
eprint(f"[event_log] ERROR appending event: {error}")
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
eprint(f"[event_log] ERROR serializing event: {e}")
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_current_state(context_id: str, project_root: Path = None) -> ContextState:
|
|
175
|
+
"""
|
|
176
|
+
Compute current context state by replaying events.
|
|
177
|
+
|
|
178
|
+
This is the canonical way to determine current state -
|
|
179
|
+
everything is derived from events.jsonl.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
context_id: Context identifier
|
|
183
|
+
project_root: Project root directory (default: cwd)
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
ContextState representing current state
|
|
187
|
+
"""
|
|
188
|
+
events = read_events(context_id, project_root)
|
|
189
|
+
|
|
190
|
+
state = ContextState(id=context_id)
|
|
191
|
+
tasks_map: Dict[str, Task] = {}
|
|
192
|
+
|
|
193
|
+
for event in events:
|
|
194
|
+
event_type = event.get("event")
|
|
195
|
+
timestamp = event.get("timestamp")
|
|
196
|
+
|
|
197
|
+
# Update last_active for any event
|
|
198
|
+
state.last_active = timestamp
|
|
199
|
+
|
|
200
|
+
if event_type == EVENT_CONTEXT_CREATED:
|
|
201
|
+
state.summary = event.get("summary", "")
|
|
202
|
+
state.method = event.get("method")
|
|
203
|
+
state.tags = event.get("tags", [])
|
|
204
|
+
state.created_at = timestamp
|
|
205
|
+
|
|
206
|
+
elif event_type == EVENT_CONTEXT_COMPLETED:
|
|
207
|
+
state.status = "completed"
|
|
208
|
+
|
|
209
|
+
elif event_type == EVENT_CONTEXT_REOPENED:
|
|
210
|
+
state.status = "active"
|
|
211
|
+
|
|
212
|
+
elif event_type == EVENT_METADATA_UPDATED:
|
|
213
|
+
if "summary" in event:
|
|
214
|
+
state.summary = event["summary"]
|
|
215
|
+
if "tags" in event:
|
|
216
|
+
state.tags = event["tags"]
|
|
217
|
+
if "method" in event:
|
|
218
|
+
state.method = event["method"]
|
|
219
|
+
|
|
220
|
+
elif event_type == EVENT_TASK_ADDED:
|
|
221
|
+
task_id = event.get("task_id")
|
|
222
|
+
if task_id:
|
|
223
|
+
tasks_map[task_id] = Task(
|
|
224
|
+
id=task_id,
|
|
225
|
+
subject=event.get("subject", ""),
|
|
226
|
+
description=event.get("description", ""),
|
|
227
|
+
active_form=event.get("activeForm", ""),
|
|
228
|
+
status="pending"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
elif event_type == EVENT_TASK_STARTED:
|
|
232
|
+
task_id = event.get("task_id")
|
|
233
|
+
if task_id and task_id in tasks_map:
|
|
234
|
+
tasks_map[task_id].status = "in_progress"
|
|
235
|
+
|
|
236
|
+
elif event_type == EVENT_TASK_COMPLETED:
|
|
237
|
+
task_id = event.get("task_id")
|
|
238
|
+
if task_id and task_id in tasks_map:
|
|
239
|
+
task = tasks_map[task_id]
|
|
240
|
+
task.status = "completed"
|
|
241
|
+
task.evidence = event.get("evidence", "")
|
|
242
|
+
task.work_summary = event.get("work_summary", "")
|
|
243
|
+
task.files_changed = event.get("files_changed", [])
|
|
244
|
+
|
|
245
|
+
elif event_type == EVENT_TASK_BLOCKED:
|
|
246
|
+
task_id = event.get("task_id")
|
|
247
|
+
if task_id and task_id in tasks_map:
|
|
248
|
+
tasks_map[task_id].status = "blocked"
|
|
249
|
+
tasks_map[task_id].blocked_reason = event.get("reason", "")
|
|
250
|
+
|
|
251
|
+
elif event_type == EVENT_NOTE_ADDED:
|
|
252
|
+
note = event.get("content", "")
|
|
253
|
+
if note:
|
|
254
|
+
state.notes.append(note)
|
|
255
|
+
|
|
256
|
+
elif event_type == EVENT_PLAN_CREATED:
|
|
257
|
+
state.plan_status = "pending_implementation"
|
|
258
|
+
state.plan_path = event.get("path")
|
|
259
|
+
state.plan_hash = event.get("hash")
|
|
260
|
+
|
|
261
|
+
elif event_type == EVENT_PLAN_IMPLEMENTATION_STARTED:
|
|
262
|
+
state.plan_status = "implementing"
|
|
263
|
+
|
|
264
|
+
elif event_type == EVENT_PLAN_COMPLETED:
|
|
265
|
+
state.plan_status = "none"
|
|
266
|
+
state.plan_path = None
|
|
267
|
+
state.plan_hash = None
|
|
268
|
+
|
|
269
|
+
# Convert tasks map to list
|
|
270
|
+
state.tasks = list(tasks_map.values())
|
|
271
|
+
|
|
272
|
+
return state
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def are_all_tasks_completed(context_id: str, project_root: Path = None) -> bool:
|
|
276
|
+
"""
|
|
277
|
+
Check if all tasks in a context are completed.
|
|
278
|
+
|
|
279
|
+
Useful for suggesting context completion to user.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
context_id: Context identifier
|
|
283
|
+
project_root: Project root directory (default: cwd)
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
True if all tasks are completed (or no tasks exist)
|
|
287
|
+
"""
|
|
288
|
+
state = get_current_state(context_id, project_root)
|
|
289
|
+
|
|
290
|
+
if not state.tasks:
|
|
291
|
+
return True # No tasks = trivially complete
|
|
292
|
+
|
|
293
|
+
return all(task.status == "completed" for task in state.tasks)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_pending_tasks(context_id: str, project_root: Path = None) -> List[Task]:
|
|
297
|
+
"""
|
|
298
|
+
Get all non-completed tasks from a context.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
context_id: Context identifier
|
|
302
|
+
project_root: Project root directory (default: cwd)
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
List of tasks that are not completed
|
|
306
|
+
"""
|
|
307
|
+
state = get_current_state(context_id, project_root)
|
|
308
|
+
return [t for t in state.tasks if t.status != "completed"]
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Plan archive utilities for context management.
|
|
2
|
+
|
|
3
|
+
Provides functions for archiving plans to context folders and
|
|
4
|
+
managing plan lifecycle.
|
|
5
|
+
|
|
6
|
+
Used by:
|
|
7
|
+
- ExitPlanMode hook to archive approved plans
|
|
8
|
+
- SessionStart to detect pending implementations
|
|
9
|
+
"""
|
|
10
|
+
import hashlib
|
|
11
|
+
import shutil
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, Tuple
|
|
15
|
+
|
|
16
|
+
from .context_manager import (
|
|
17
|
+
Context,
|
|
18
|
+
create_context,
|
|
19
|
+
get_context,
|
|
20
|
+
get_all_contexts,
|
|
21
|
+
update_plan_status,
|
|
22
|
+
)
|
|
23
|
+
from .event_log import append_event, EVENT_PLAN_CREATED
|
|
24
|
+
from ..base.atomic_write import atomic_write
|
|
25
|
+
from ..base.constants import get_context_plans_dir
|
|
26
|
+
from ..base.utils import eprint, now_iso, sanitize_title
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def archive_plan_to_context(
|
|
30
|
+
plan_path: str,
|
|
31
|
+
context_id: str,
|
|
32
|
+
project_root: Path = None
|
|
33
|
+
) -> Tuple[Optional[str], Optional[str]]:
|
|
34
|
+
"""
|
|
35
|
+
Archive plan to context's plans folder.
|
|
36
|
+
|
|
37
|
+
Actions:
|
|
38
|
+
1. Copy plan to _output/contexts/<context_id>/plans/<date>-<slug>.md
|
|
39
|
+
2. Compute plan hash for change detection
|
|
40
|
+
3. Update context.json: in_flight.mode = "pending_implementation"
|
|
41
|
+
4. Update context.json: in_flight.artifact_path = archived path
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
plan_path: Path to the plan file to archive
|
|
45
|
+
context_id: Target context ID
|
|
46
|
+
project_root: Project root directory
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Tuple of (archived_path, plan_hash) or (None, None) on error
|
|
50
|
+
"""
|
|
51
|
+
plan_file = Path(plan_path)
|
|
52
|
+
if not plan_file.exists():
|
|
53
|
+
eprint(f"[plan_archive] Plan file not found: {plan_path}")
|
|
54
|
+
return None, None
|
|
55
|
+
|
|
56
|
+
# Read plan content
|
|
57
|
+
try:
|
|
58
|
+
plan_content = plan_file.read_text(encoding='utf-8')
|
|
59
|
+
except Exception as e:
|
|
60
|
+
eprint(f"[plan_archive] Failed to read plan: {e}")
|
|
61
|
+
return None, None
|
|
62
|
+
|
|
63
|
+
# Compute hash for change detection
|
|
64
|
+
plan_hash = hashlib.sha256(plan_content.encode('utf-8')).hexdigest()[:12]
|
|
65
|
+
|
|
66
|
+
# Create plans directory
|
|
67
|
+
plans_dir = get_context_plans_dir(context_id, project_root)
|
|
68
|
+
plans_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
# Generate archive filename: YYYY-MM-DD-<slug>.md
|
|
71
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
72
|
+
slug = sanitize_title(plan_file.stem, max_len=30)
|
|
73
|
+
archive_name = f"{date_str}-{slug}.md"
|
|
74
|
+
archive_path = plans_dir / archive_name
|
|
75
|
+
|
|
76
|
+
# Handle name collision
|
|
77
|
+
counter = 2
|
|
78
|
+
while archive_path.exists():
|
|
79
|
+
archive_name = f"{date_str}-{slug}-{counter}.md"
|
|
80
|
+
archive_path = plans_dir / archive_name
|
|
81
|
+
counter += 1
|
|
82
|
+
|
|
83
|
+
# Write archived plan
|
|
84
|
+
success, error = atomic_write(archive_path, plan_content)
|
|
85
|
+
if not success:
|
|
86
|
+
eprint(f"[plan_archive] Failed to write archive: {error}")
|
|
87
|
+
return None, None
|
|
88
|
+
|
|
89
|
+
# Update context plan status
|
|
90
|
+
update_plan_status(
|
|
91
|
+
context_id,
|
|
92
|
+
status="pending_implementation",
|
|
93
|
+
path=str(archive_path),
|
|
94
|
+
hash=plan_hash,
|
|
95
|
+
project_root=project_root
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
eprint(f"[plan_archive] Archived plan to: {archive_path}")
|
|
99
|
+
return str(archive_path), plan_hash
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_active_context_for_plan(
|
|
103
|
+
plan_path: str,
|
|
104
|
+
project_root: Path = None
|
|
105
|
+
) -> Optional[str]:
|
|
106
|
+
"""
|
|
107
|
+
Determine which context a plan belongs to.
|
|
108
|
+
|
|
109
|
+
Logic:
|
|
110
|
+
1. If exactly one active context exists -> use it
|
|
111
|
+
2. Check if plan path contains context hints
|
|
112
|
+
3. Return None if ambiguous
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
plan_path: Path to plan file
|
|
116
|
+
project_root: Project root directory
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Context ID or None if cannot determine
|
|
120
|
+
"""
|
|
121
|
+
active_contexts = get_all_contexts(status="active", project_root=project_root)
|
|
122
|
+
|
|
123
|
+
# If exactly one active context, use it
|
|
124
|
+
if len(active_contexts) == 1:
|
|
125
|
+
return active_contexts[0].id
|
|
126
|
+
|
|
127
|
+
# Check if plan path contains a context ID
|
|
128
|
+
plan_path_lower = plan_path.lower()
|
|
129
|
+
for ctx in active_contexts:
|
|
130
|
+
if ctx.id.lower() in plan_path_lower:
|
|
131
|
+
return ctx.id
|
|
132
|
+
|
|
133
|
+
# Check plan filename for context hints
|
|
134
|
+
plan_file = Path(plan_path)
|
|
135
|
+
filename_lower = plan_file.stem.lower()
|
|
136
|
+
for ctx in active_contexts:
|
|
137
|
+
if ctx.id.lower() in filename_lower:
|
|
138
|
+
return ctx.id
|
|
139
|
+
|
|
140
|
+
# If no active contexts, return None (caller should create new context)
|
|
141
|
+
if not active_contexts:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# Multiple active contexts, cannot determine
|
|
145
|
+
eprint(f"[plan_archive] Multiple active contexts, cannot determine target")
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def create_context_from_plan(
|
|
150
|
+
plan_path: str,
|
|
151
|
+
project_root: Path = None
|
|
152
|
+
) -> Optional[str]:
|
|
153
|
+
"""
|
|
154
|
+
Create a new context based on a plan file.
|
|
155
|
+
|
|
156
|
+
Extracts context ID and summary from plan filename/content.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
plan_path: Path to plan file
|
|
160
|
+
project_root: Project root directory
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Created context ID or None on error
|
|
164
|
+
"""
|
|
165
|
+
plan_file = Path(plan_path)
|
|
166
|
+
if not plan_file.exists():
|
|
167
|
+
eprint(f"[plan_archive] Plan file not found: {plan_path}")
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
# Generate context ID from plan filename
|
|
171
|
+
context_id = sanitize_title(plan_file.stem, max_len=50)
|
|
172
|
+
|
|
173
|
+
# Try to extract summary from plan content
|
|
174
|
+
try:
|
|
175
|
+
content = plan_file.read_text(encoding='utf-8')
|
|
176
|
+
# Look for first heading as summary
|
|
177
|
+
for line in content.splitlines():
|
|
178
|
+
line = line.strip()
|
|
179
|
+
if line.startswith('#'):
|
|
180
|
+
summary = line.lstrip('#').strip()[:100]
|
|
181
|
+
break
|
|
182
|
+
else:
|
|
183
|
+
summary = f"Implementation of {plan_file.stem}"
|
|
184
|
+
except Exception:
|
|
185
|
+
summary = f"Implementation of {plan_file.stem}"
|
|
186
|
+
|
|
187
|
+
# Create the context
|
|
188
|
+
try:
|
|
189
|
+
context = create_context(
|
|
190
|
+
context_id=context_id,
|
|
191
|
+
summary=summary,
|
|
192
|
+
method="cc-native",
|
|
193
|
+
project_root=project_root
|
|
194
|
+
)
|
|
195
|
+
return context.id
|
|
196
|
+
except ValueError as e:
|
|
197
|
+
eprint(f"[plan_archive] Failed to create context: {e}")
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def mark_plan_implementation_started(
|
|
202
|
+
context_id: str,
|
|
203
|
+
project_root: Path = None
|
|
204
|
+
) -> bool:
|
|
205
|
+
"""
|
|
206
|
+
Mark that plan implementation has started.
|
|
207
|
+
|
|
208
|
+
Called by SessionStart after detecting pending_implementation.
|
|
209
|
+
Prevents re-triggering on subsequent /clear commands.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
context_id: Context identifier
|
|
213
|
+
project_root: Project root directory
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
True if successful
|
|
217
|
+
"""
|
|
218
|
+
context = update_plan_status(
|
|
219
|
+
context_id,
|
|
220
|
+
status="implementing",
|
|
221
|
+
project_root=project_root
|
|
222
|
+
)
|
|
223
|
+
return context is not None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def mark_plan_completed(
|
|
227
|
+
context_id: str,
|
|
228
|
+
project_root: Path = None
|
|
229
|
+
) -> bool:
|
|
230
|
+
"""
|
|
231
|
+
Mark that plan has been fully implemented.
|
|
232
|
+
|
|
233
|
+
Called when all plan tasks are completed.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
context_id: Context identifier
|
|
237
|
+
project_root: Project root directory
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
True if successful
|
|
241
|
+
"""
|
|
242
|
+
context = update_plan_status(
|
|
243
|
+
context_id,
|
|
244
|
+
status="none",
|
|
245
|
+
project_root=project_root
|
|
246
|
+
)
|
|
247
|
+
return context is not None
|