Skip to content

Commit 0141f9e

Browse files
committed
🎼 Initial commit: Conductor-Score v1.0.1 (Warp-Optimized, Cross-Platform, Production-Ready)
- Major Features: - Warp terminal detection and launch support (macOS, Linux, Windows) - 'Recommended Terminals' section in README.md and docs/USAGE.md - Fallbacks: iTerm2, Kitty, Alacritty, Windows Terminal, tmux, screen - Storage footprint guidance and cleanup tips - Reliability & Schema: - All scripts and examples use files_locked (no files_involved) - Atomic file writes with flush/fsync for workflow-state.json - Agent ID collision prevention (random slug) - All scripts referenced in Makefile/workflows are present and pass py_compile - UX & Helper Improvements: - gtopen and gtwarp helpers in docs/worktree-helper.sh - Bootstrap script prints correct open command for best terminal on each OS - Documentation: - CHANGELOG.md and VERSION updated to 1.0.1 (2024-07-22) - README.md and USAGE.md clarify Conductor app is macOS-only (as of 2024-07-22) - Storage footprint and cleanup best practices - Ready for: - macOS, Linux, Windows (multi-terminal, tmux, screen, Warp) - CI and GitHub Actions workflows - All user stories in the v1 launch brief See CHANGELOG.md for full details.
0 parents  commit 0141f9e

35 files changed

+5998
-0
lines changed

.conductor/config.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
version: "1.0.0"
2+
project_name: "my-project"
3+
docs_directory: "docs/"
4+
task_management: "hybrid"
5+
6+
roles:
7+
default: "dev"
8+
specialized: []
9+
10+
conflict_prevention:
11+
use_worktrees: true
12+
file_locking: true
13+
14+
github:
15+
use_issues: true
16+
use_actions: true
17+
18+
agent_settings:
19+
heartbeat_interval: 600 # 10 minutes
20+
idle_timeout: 1800 # 30 minutes
21+
max_concurrent: 20
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
#!/usr/bin/env python3
2+
"""Archive completed tasks and clean up workflow state"""
3+
4+
import json
5+
import sys
6+
from pathlib import Path
7+
from datetime import datetime, timedelta
8+
9+
10+
def load_state():
11+
"""Load workflow state"""
12+
state_file = Path('.conductor/workflow-state.json')
13+
if not state_file.exists():
14+
print("❌ Workflow state file not found")
15+
sys.exit(1)
16+
17+
try:
18+
with open(state_file, 'r') as f:
19+
return json.load(f)
20+
except json.JSONDecodeError:
21+
print("❌ Invalid workflow state file")
22+
sys.exit(1)
23+
24+
25+
def save_state(state):
26+
"""Save workflow state"""
27+
state_file = Path('.conductor/workflow-state.json')
28+
try:
29+
with open(state_file, 'w') as f:
30+
json.dump(state, f, indent=2)
31+
except Exception as e:
32+
print(f"❌ Failed to save state: {e}")
33+
sys.exit(1)
34+
35+
36+
def archive_completed_tasks(state, max_age_days=30):
37+
"""Archive completed tasks older than max_age_days"""
38+
if 'completed_work' not in state:
39+
state['completed_work'] = []
40+
41+
current_time = datetime.utcnow()
42+
cutoff_date = current_time - timedelta(days=max_age_days)
43+
44+
# Separate recent and old completed work
45+
recent_completed = []
46+
archived_count = 0
47+
48+
for work in state['completed_work']:
49+
completed_at_str = work.get('completed_at')
50+
if completed_at_str:
51+
try:
52+
completed_at = datetime.fromisoformat(completed_at_str.replace('Z', '+00:00'))
53+
if completed_at >= cutoff_date:
54+
recent_completed.append(work)
55+
else:
56+
archived_count += 1
57+
except ValueError:
58+
# Keep items with invalid dates
59+
recent_completed.append(work)
60+
else:
61+
# Keep items without completion dates
62+
recent_completed.append(work)
63+
64+
# Update completed work to only include recent items
65+
state['completed_work'] = recent_completed
66+
67+
if archived_count > 0:
68+
print(f"📦 Archived {archived_count} completed task(s) older than {max_age_days} days")
69+
else:
70+
print("ℹ️ No old completed tasks to archive")
71+
72+
return archived_count
73+
74+
75+
def clean_stale_active_work(state, stale_timeout_minutes=30):
76+
"""Move stale active work to completed with appropriate status"""
77+
if 'active_work' not in state:
78+
state['active_work'] = {}
79+
80+
if 'completed_work' not in state:
81+
state['completed_work'] = []
82+
83+
current_time = datetime.utcnow()
84+
stale_cutoff = current_time - timedelta(minutes=stale_timeout_minutes)
85+
86+
active_work = state['active_work']
87+
stale_agents = []
88+
89+
for agent_id, work in list(active_work.items()):
90+
heartbeat_str = work.get('heartbeat')
91+
if heartbeat_str:
92+
try:
93+
heartbeat = datetime.fromisoformat(heartbeat_str.replace('Z', '+00:00'))
94+
if heartbeat < stale_cutoff:
95+
stale_agents.append(agent_id)
96+
except ValueError:
97+
# Consider items with invalid heartbeat as stale
98+
stale_agents.append(agent_id)
99+
else:
100+
# Consider items without heartbeat as stale
101+
stale_agents.append(agent_id)
102+
103+
# Move stale work to completed
104+
cleaned_count = 0
105+
for agent_id in stale_agents:
106+
work = active_work.pop(agent_id)
107+
108+
# Mark as abandoned
109+
work['status'] = 'abandoned'
110+
work['completed_at'] = current_time.isoformat()
111+
work['abandonment_reason'] = 'stale_heartbeat'
112+
113+
state['completed_work'].append(work)
114+
cleaned_count += 1
115+
116+
if cleaned_count > 0:
117+
print(f"🧹 Moved {cleaned_count} stale agent(s) to completed work")
118+
else:
119+
print("ℹ️ No stale active work found")
120+
121+
return cleaned_count
122+
123+
124+
def optimize_state_file(state):
125+
"""Optimize state file by removing redundant data and organizing"""
126+
# Ensure all required sections exist
127+
if 'active_work' not in state:
128+
state['active_work'] = {}
129+
if 'available_tasks' not in state:
130+
state['available_tasks'] = []
131+
if 'completed_work' not in state:
132+
state['completed_work'] = []
133+
if 'system_status' not in state:
134+
state['system_status'] = {}
135+
136+
# Sort tasks by creation date (newest first)
137+
for task_list in [state['available_tasks'], state['completed_work']]:
138+
if isinstance(task_list, list):
139+
task_list.sort(key=lambda x: x.get('created_at', ''), reverse=True)
140+
141+
# Update system status
142+
state['system_status']['last_cleanup'] = datetime.utcnow().isoformat()
143+
144+
print("✨ Optimized state file structure")
145+
146+
147+
def generate_cleanup_report(state, archived_count, cleaned_count):
148+
"""Generate a summary report of cleanup actions"""
149+
print("\n📊 Cleanup Summary")
150+
print("=" * 30)
151+
152+
current_metrics = {
153+
'active_agents': len(state.get('active_work', {})),
154+
'available_tasks': len(state.get('available_tasks', [])),
155+
'completed_tasks': len(state.get('completed_work', [])),
156+
'archived_tasks': archived_count,
157+
'cleaned_stale': cleaned_count
158+
}
159+
160+
for metric, value in current_metrics.items():
161+
print(f"{metric.replace('_', ' ').title()}: {value}")
162+
163+
print(f"\nCleanup completed at: {datetime.utcnow().isoformat()}")
164+
165+
166+
def main():
167+
import argparse
168+
169+
parser = argparse.ArgumentParser(description="Archive completed tasks and clean up state")
170+
parser.add_argument("--max-age", type=int, default=30,
171+
help="Maximum age in days for completed tasks (default: 30)")
172+
parser.add_argument("--stale-timeout", type=int, default=30,
173+
help="Stale timeout in minutes for active work (default: 30)")
174+
parser.add_argument("--dry-run", action="store_true",
175+
help="Show what would be done without making changes")
176+
177+
args = parser.parse_args()
178+
179+
print("🧹 Starting cleanup and archival process...")
180+
181+
# Load current state
182+
state = load_state()
183+
184+
if args.dry_run:
185+
print("(DRY RUN - no changes will be made)")
186+
187+
# Archive old completed tasks
188+
archived_count = 0 if args.dry_run else archive_completed_tasks(state, args.max_age)
189+
190+
# Clean stale active work
191+
cleaned_count = 0 if args.dry_run else clean_stale_active_work(state, args.stale_timeout)
192+
193+
# Optimize state file
194+
if not args.dry_run:
195+
optimize_state_file(state)
196+
save_state(state)
197+
198+
# Generate report
199+
generate_cleanup_report(state, archived_count, cleaned_count)
200+
201+
if args.dry_run:
202+
print("\nRun without --dry-run to perform cleanup")
203+
else:
204+
print("\n✅ Cleanup completed successfully")
205+
206+
207+
if __name__ == "__main__":
208+
main()

0 commit comments

Comments
 (0)