@ikieaneh/opencode-kit 0.6.0 → 0.6.2

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.
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ """postflight helper — batch all contract/telemetry/STATE.md operations in one call.
3
+
4
+ Replaces 11 separate inline Python invocations from postflight.sh with a single
5
+ script that handles: contract read/migration, phase telemetry, summary update,
6
+ STATE.md generation, and contract file persistence.
7
+
8
+ Usage:
9
+ postflight.py CONTRACT_FILE TELEMETRY_DIR STATE_FILE STATE_BACKUP_DIR TEMPLATE_FILE [NEW_STATE]
10
+
11
+ Returns JSON to stdout with extracted values for shell-level echo/log messages.
12
+ """
13
+ import json
14
+ import os
15
+ import sys
16
+ import datetime
17
+
18
+
19
+ def main() -> int:
20
+ # --- Parse arguments ---
21
+ contract_file = sys.argv[1] if len(sys.argv) > 1 else '.opencode/orchestration/contract.json'
22
+ telemetry_dir = sys.argv[2] if len(sys.argv) > 2 else '.opencode/telemetry'
23
+ state_file = sys.argv[3] if len(sys.argv) > 3 else 'STATE.md'
24
+ state_backup_dir = sys.argv[4] if len(sys.argv) > 4 else '.opencode/state'
25
+ template_file = sys.argv[5] if len(sys.argv) > 5 else '.opencode/templates/contract.json'
26
+ new_state = sys.argv[6] if len(sys.argv) > 6 else None
27
+
28
+ start_time_file = os.path.join(telemetry_dir, '.phase_start')
29
+ phases_file = os.path.join(telemetry_dir, 'phases.jsonl')
30
+ summary_file = os.path.join(telemetry_dir, 'summary.json')
31
+
32
+ now = datetime.datetime.now(datetime.UTC)
33
+ now_iso = now.strftime('%Y-%m-%dT%H:%M:%SZ')
34
+
35
+ # --- Load contract (file or template fallback) ---
36
+ contract = {}
37
+ if os.path.exists(contract_file):
38
+ with open(contract_file) as f:
39
+ contract = json.load(f)
40
+ elif os.path.exists(template_file):
41
+ with open(template_file) as f:
42
+ contract = json.load(f)
43
+
44
+ prev_state = contract.get('state', 'INIT')
45
+
46
+ # --- Update state if provided ---
47
+ if new_state:
48
+ contract['state'] = new_state
49
+
50
+ current_state = contract.get('state', 'UNKNOWN')
51
+
52
+ # --- Read start time and calculate phase elapsed ---
53
+ os.makedirs(telemetry_dir, exist_ok=True)
54
+ phase_elapsed_ms = 0
55
+ if os.path.exists(start_time_file):
56
+ with open(start_time_file) as f:
57
+ raw = f.read().strip()
58
+ if raw:
59
+ try:
60
+ start_ts = int(raw)
61
+ phase_elapsed_ms = int((now.timestamp() - start_ts) * 1000)
62
+ except ValueError:
63
+ pass
64
+ os.remove(start_time_file)
65
+
66
+ # --- Read last 'to' state from phases.jsonl for telemetry ---
67
+ last_to_state = 'INIT'
68
+ if os.path.exists(phases_file):
69
+ lines = [l.strip() for l in open(phases_file) if l.strip()]
70
+ if lines:
71
+ try:
72
+ last_to_state = json.loads(lines[-1]).get('to', 'INIT')
73
+ except json.JSONDecodeError:
74
+ pass
75
+
76
+ # --- Record phase telemetry ---
77
+ phase_entry = {
78
+ 'ts': now_iso,
79
+ 'from': last_to_state,
80
+ 'to': current_state,
81
+ 'elapsed_ms': phase_elapsed_ms,
82
+ }
83
+ with open(phases_file, 'a') as f:
84
+ f.write(json.dumps(phase_entry) + '\n')
85
+
86
+ # --- Contract migration (merge missing fields from template) ---
87
+ migrated = False
88
+ if os.path.exists(template_file):
89
+ with open(template_file) as f:
90
+ template = json.load(f)
91
+
92
+ old_ver = contract.get('contract_version', '0.0.0')
93
+ new_ver = template.get('contract_version', '0.5.2')
94
+
95
+ needs_migration = (
96
+ old_ver != new_ver
97
+ or not all(k in contract for k in ['state', 'requirements', 'governance', 'score'])
98
+ )
99
+
100
+ if needs_migration:
101
+ for key in template:
102
+ if key not in contract:
103
+ contract[key] = template[key]
104
+ if 'extension_skills' not in contract.get('governance', {}):
105
+ contract.setdefault('governance', {})['extension_skills'] = []
106
+ contract['contract_version'] = new_ver
107
+ migrated = True
108
+
109
+ # --- Write contract to primary location ---
110
+ os.makedirs(os.path.dirname(contract_file) or '.', exist_ok=True)
111
+ with open(contract_file, 'w') as f:
112
+ json.dump(contract, f, indent=2)
113
+
114
+ # --- Backup contract to state backup dir ---
115
+ os.makedirs(state_backup_dir, exist_ok=True)
116
+ with open(os.path.join(state_backup_dir, 'contract.json'), 'w') as f:
117
+ json.dump(contract, f, indent=2)
118
+
119
+ # --- Update telemetry summary.json ---
120
+ total_ms = 0
121
+ phases = []
122
+ if os.path.exists(phases_file):
123
+ with open(phases_file) as f:
124
+ for line in f:
125
+ line = line.strip()
126
+ if not line:
127
+ continue
128
+ try:
129
+ entry = json.loads(line)
130
+ total_ms += entry.get('elapsed_ms', 0)
131
+ phases.append(entry.get('to', ''))
132
+ except json.JSONDecodeError:
133
+ pass
134
+
135
+ summary = {
136
+ 'phases_completed': phases,
137
+ 'total_elapsed_ms': total_ms,
138
+ 'total_elapsed_s': round(total_ms / 1000, 1),
139
+ 'updated_at': now_iso,
140
+ }
141
+ with open(summary_file, 'w') as f:
142
+ json.dump(summary, f, indent=2)
143
+
144
+ # --- Extract values for STATE.md ---
145
+ retry = contract.get('retry', {})
146
+ score_obj = contract.get('score', {})
147
+ state_val = contract.get('state', 'UNKNOWN')
148
+ phase_val = retry.get('current_phase', 'none')
149
+ score_combined = str(score_obj.get('combined', '?'))
150
+
151
+ # Issues / blockers
152
+ issues = retry.get('issues', [])
153
+ issues_blockers = '\n'.join(f'- {i}' for i in issues) if issues else 'None'
154
+
155
+ # ADRs (last 3)
156
+ adr_log = contract.get('decisions', {}).get('adr_log', [])
157
+ if adr_log:
158
+ adr_lines = '\n'.join(
159
+ f'- {e.get("id", "?")}: {e.get("title", "")}' for e in adr_log[-3:]
160
+ )
161
+ else:
162
+ adr_lines = 'No ADRs recorded'
163
+
164
+ # Last completed phase
165
+ metrics_phases = contract.get('metrics', {}).get('phases_completed', [])
166
+ last_phase = metrics_phases[-1] if metrics_phases else 'INIT'
167
+
168
+ # --- Write STATE.md ---
169
+ state_md = (
170
+ '# Project State\n'
171
+ '\n'
172
+ '## Current Focus\n'
173
+ f'Agent orchestration — {state_val} (phase: {phase_val}). Score: {score_combined}.\n'
174
+ '\n'
175
+ '## Known Blockers\n'
176
+ f'{issues_blockers}\n'
177
+ '\n'
178
+ '## Active Decisions\n'
179
+ f'{adr_lines}\n'
180
+ '\n'
181
+ '## Recent Changes\n'
182
+ f'- Last state transition: {last_phase}\n'
183
+ )
184
+ state_dir = os.path.dirname(state_file)
185
+ if state_dir:
186
+ os.makedirs(state_dir, exist_ok=True)
187
+ with open(state_file, 'w') as f:
188
+ f.write(state_md)
189
+
190
+ # --- Output JSON summary for shell script consumption ---
191
+ output = {
192
+ 'state': state_val,
193
+ 'phase': phase_val,
194
+ 'score': score_combined,
195
+ 'prev_state': prev_state,
196
+ 'current_state': current_state,
197
+ 'phase_elapsed_ms': phase_elapsed_ms,
198
+ 'phase_elapsed_s': round(phase_elapsed_ms / 1000, 1),
199
+ 'migrated': migrated,
200
+ 'contract_version': contract.get('contract_version', '?'),
201
+ 'phases_count': len(phases),
202
+ 'total_elapsed_s': summary['total_elapsed_s'],
203
+ 'issues_count': len(issues),
204
+ 'last_to_state': last_to_state,
205
+ }
206
+ print(json.dumps(output))
207
+ return 0
208
+
209
+
210
+ if __name__ == '__main__':
211
+ sys.exit(main())
package/src/postflight.sh CHANGED
@@ -11,7 +11,6 @@ CONTRACT_KEY="orchestration-contract"
11
11
  CONTRACT_FILE=".opencode/orchestration/contract.json"
12
12
  STATE_FILE="STATE.md"
13
13
  TELEMETRY_DIR=".opencode/telemetry"
14
- START_TIME_FILE=".opencode/telemetry/.phase_start"
15
14
  STATE_BACKUP_DIR=".opencode/state"
16
15
  TEMPLATE_FILE=".opencode/templates/contract.json"
17
16
 
@@ -19,171 +18,65 @@ mkdir -p "$TELEMETRY_DIR" "$STATE_BACKUP_DIR"
19
18
 
20
19
  echo "[opencode-kit] Post-flight: persisting state..."
21
20
 
22
- # --- Telemetry: record phase completion ---
23
- mkdir -p "$TELEMETRY_DIR"
24
- PHASE_START=$(cat "$START_TIME_FILE" 2>/dev/null || echo "")
25
- if [ -n "$PHASE_START" ]; then
26
- PHASE_ELAPSED=$(( $(date +%s) - PHASE_START ))
27
- # Read current state from contract
28
- if [ -f "$CONTRACT_FILE" ]; then
29
- CURRENT_STATE=$($PYTHON_CMD -c "
30
- import json
31
- with open('$CONTRACT_FILE') as f: d=json.load(f)
32
- print(d.get('state','UNKNOWN'))
33
- " 2>/dev/null || echo "UNKNOWN")
34
- PREV_STATE=""
35
- [ -f "$TELEMETRY_DIR/phases.jsonl" ] && PREV_STATE=$(tail -1 "$TELEMETRY_DIR/phases.jsonl" 2>/dev/null | $PYTHON_CMD -c "import sys,json; print(json.load(sys.stdin).get('to','INIT'))" 2>/dev/null || echo "INIT")
36
- echo "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"from\":\"$PREV_STATE\",\"to\":\"$CURRENT_STATE\",\"elapsed_ms\":$((PHASE_ELAPSED * 1000))}" >> "$TELEMETRY_DIR/phases.jsonl"
37
- echo " 📊 Telemetry: $PREV_STATE → $CURRENT_STATE (${PHASE_ELAPSED}s)"
38
- fi
39
- rm -f "$START_TIME_FILE"
21
+ # --- Resolve contract: try lean-ctx first, fall back to file ---
22
+ LEAN_CTX_CONTRACT=$(lean-ctx ctx_knowledge recall --query "$CONTRACT_KEY" 2>/dev/null || true)
23
+ if [ -n "$LEAN_CTX_CONTRACT" ]; then
24
+ echo "$LEAN_CTX_CONTRACT" > "$CONTRACT_FILE"
40
25
  fi
41
26
 
42
- # --- Step 1: Read contract (try lean-ctx first, fall back to file) ---
43
- CURRENT_CONTRACT=$(lean-ctx ctx_knowledge recall --query "$CONTRACT_KEY" 2>/dev/null || cat "$CONTRACT_FILE" 2>/dev/null || echo "")
44
- if [ -z "$CURRENT_CONTRACT" ]; then
45
- echo " ⚠️ No contract found in lean-ctx or file. Creating new from template..."
46
- if [ -f "$TEMPLATE_FILE" ]; then
47
- CURRENT_CONTRACT=$(cat "$TEMPLATE_FILE")
27
+ # --- Single Python call: batch all contract/telemetry/STATE.md operations ---
28
+ PYTHON_OUTPUT=""
29
+ STATE=""
30
+ if [ -n "$PYTHON_CMD" ]; then
31
+ PYTHON_OUTPUT=$($PYTHON_CMD "$SCRIPT_DIR/postflight.py" \
32
+ "$CONTRACT_FILE" "$TELEMETRY_DIR" "$STATE_FILE" \
33
+ "$STATE_BACKUP_DIR" "$TEMPLATE_FILE" \
34
+ )
35
+
36
+ # Single Python parse call: extract all values as pipe-delimited string
37
+ IFS='|' read -r STATE PHASE SCORE PREV_STATE CURRENT_STATE ELAPSED_S MIGRATED CONTRACT_VER PHASES_COUNT TOTAL_ELAPSED_S LAST_TO_STATE <<< \
38
+ "$(echo "$PYTHON_OUTPUT" | $PYTHON_CMD -c "
39
+ import sys, json
40
+ d = json.load(sys.stdin)
41
+ print('|'.join([str(d.get(k, '')) for k in [
42
+ 'state', 'phase', 'score', 'prev_state', 'current_state',
43
+ 'phase_elapsed_s', 'migrated', 'contract_version',
44
+ 'phases_count', 'total_elapsed_s', 'last_to_state'
45
+ ]]))
46
+ " 2>/dev/null || echo "UNKNOWN|none|?|INIT|UNKNOWN|0|false|?|0|0|INIT")"
47
+
48
+ # --- Echo status messages ---
49
+ echo " 📊 Telemetry: $PREV_STATE → $CURRENT_STATE (${ELAPSED_S}s)"
50
+
51
+ if [ "$MIGRATED" = "true" ]; then
52
+ echo " 🔄 Contract migrated to v${CONTRACT_VER}"
48
53
  fi
49
- fi
50
-
51
- # --- Step 1b: Contract migration (auto-upgrade old schema) ---
52
- if [ -n "$CURRENT_CONTRACT" ] && [ -f "$TEMPLATE_FILE" ] && [ -n "$PYTHON_CMD" ]; then
53
- MIGRATED=$($PYTHON_CMD -c "
54
- import json
55
54
 
56
- try:
57
- contract = json.loads('''$CURRENT_CONTRACT''')
58
- with open('$TEMPLATE_FILE') as f:
59
- template = json.load(f)
60
-
61
- # Check version
62
- old_ver = contract.get('contract_version', '0.0.0')
63
- new_ver = template.get('contract_version', '0.5.2')
64
-
65
- if old_ver == new_ver and all(k in contract for k in ['state','requirements','governance','score']):
66
- print('NO_MIGRATION')
67
- else:
68
- # Merge missing top-level fields from template
69
- for key in template:
70
- if key not in contract:
71
- contract[key] = template[key]
72
- # Merge governance.extension_skills if missing
73
- if 'extension_skills' not in contract.get('governance', {}):
74
- if 'governance' not in contract:
75
- contract['governance'] = {}
76
- contract['governance']['extension_skills'] = []
77
- # Update version
78
- contract['contract_version'] = new_ver
79
- print(json.dumps(contract))
80
- except Exception as e:
81
- print('MIGRATE_ERROR:'+str(e))
82
- " 2>/dev/null || echo "MIGRATE_ERROR")
55
+ echo " 📝 Contract state: $STATE (phase: $PHASE, score: $SCORE)"
56
+ echo " ✅ STATE.md synced"
83
57
 
84
- if [ -n "$MIGRATED" ] && [ "$MIGRATED" != "NO_MIGRATION" ] && [ "$MIGRATED" != "MIGRATE_ERROR" ]; then
85
- CURRENT_CONTRACT="$MIGRATED"
86
- echo "$MIGRATED" > "$CONTRACT_FILE"
87
- echo " 🔄 Contract migrated to v$(echo "$MIGRATED" | $PYTHON_CMD -c "import sys,json; print(json.load(sys.stdin).get('contract_version','?'))" 2>/dev/null)"
88
- fi
58
+ echo " 📈 Telemetry summary: ${PHASES_COUNT} phases, ${TOTAL_ELAPSED_S}s total"
59
+ else
60
+ echo " ⚠️ Python not available, skipping postflight processing"
89
61
  fi
90
62
 
91
- # --- Step 2: Persist (try lean-ctx first, fall back to file) ---
92
- PERSISTED=false
93
- if lean-ctx ctx_knowledge remember \
94
- category architecture \
95
- key "$CONTRACT_KEY" \
96
- value "$CURRENT_CONTRACT" 2>/dev/null; then
97
- echo " Contract persisted to lean-ctx"
98
- PERSISTED=true
63
+ # --- Persist to lean-ctx ---
64
+ if [ -n "$PYTHON_CMD" ] && [ -f "$CONTRACT_FILE" ]; then
65
+ CONTRACT_JSON=$(cat "$CONTRACT_FILE")
66
+ if lean-ctx ctx_knowledge remember \
67
+ category architecture \
68
+ key "$CONTRACT_KEY" \
69
+ value "$CONTRACT_JSON" 2>/dev/null; then
70
+ echo " ✅ Contract persisted to lean-ctx"
71
+ fi
99
72
  fi
100
73
 
101
- # File fallback: write to .opencode/state/contract.json
102
- echo "$CURRENT_CONTRACT" > "$STATE_BACKUP_DIR/contract.json"
74
+ # File fallback always written by Python script to $STATE_BACKUP_DIR/contract.json
103
75
  echo " ✅ Contract persisted to file: $STATE_BACKUP_DIR/contract.json"
104
76
 
105
- # --- Step 3: Sync STATE.md ---
106
- mkdir -p "$(dirname "$STATE_FILE")"
107
- if [ -f "$CONTRACT_FILE" ]; then
108
- STATE=$(echo "$CURRENT_CONTRACT" | $PYTHON_CMD -c "import sys,json; d=json.load(sys.stdin); print(d.get('state','UNKNOWN'))" 2>/dev/null || echo "UNKNOWN")
109
- PHASE=$(echo "$CURRENT_CONTRACT" | $PYTHON_CMD -c "import sys,json; d=json.load(sys.stdin); r=d.get('retry',{}); print(r.get('current_phase','none'))" 2>/dev/null || echo "none")
110
- SCORE=$(echo "$CURRENT_CONTRACT" | $PYTHON_CMD -c "import sys,json; d=json.load(sys.stdin); s=d.get('score',{}); print(s.get('combined','?'))" 2>/dev/null || echo "?")
111
- echo " 📝 Contract state: $STATE (phase: $PHASE, score: $SCORE)"
112
-
113
- # Create or update STATE.md with current focus
114
- cat > "$STATE_FILE" << STATEMD
115
- # Project State
116
-
117
- ## Current Focus
118
- Agent orchestration — $STATE (phase: ${PHASE:-none}). Score: $SCORE.
119
-
120
- ## Known Blockers
121
- $(echo "$CURRENT_CONTRACT" | $PYTHON_CMD -c "
122
- import sys,json
123
- d=json.load(sys.stdin)
124
- r=d.get('retry',{})
125
- issues=r.get('issues',[])
126
- if issues:
127
- for i in issues: print(f'- {i}')
128
- else:
129
- print('None')
130
- " 2>/dev/null || echo "None")
131
-
132
- ## Active Decisions
133
- $(echo "$CURRENT_CONTRACT" | $PYTHON_CMD -c "
134
- import sys,json
135
- d=json.load(sys.stdin)
136
- log=d.get('decisions',{}).get('adr_log',[])
137
- if log:
138
- for entry in log[-3:]:
139
- print(f'- {entry.get(\"id\",\"?\")}: {entry.get(\"title\",\"\")}')
140
- else:
141
- print('No ADRs recorded')
142
- " 2>/dev/null || echo "None")
143
-
144
- ## Recent Changes
145
- - Last state transition: $(echo "$CURRENT_CONTRACT" | $PYTHON_CMD -c "
146
- import sys,json
147
- d=json.load(sys.stdin)
148
- phases=d.get('metrics',{}).get('phases_completed',[])
149
- print(phases[-1] if phases else 'INIT')
150
- " 2>/dev/null || echo "INIT")
151
- STATEMD
152
- echo " ✅ STATE.md synced"
153
- fi
154
-
155
- # --- Step 4: Save ctx_session ---
77
+ # --- Save ctx_session ---
156
78
  lean-ctx ctx_session save 2>/dev/null && \
157
79
  echo " ✅ Session saved" || \
158
80
  echo " ⚠️ ctx_session save skipped (not available)"
159
81
 
160
- # --- Step 5: Update telemetry summary ---
161
- if [ -f "$TELEMETRY_DIR/phases.jsonl" ] && [ -n "$PYTHON_CMD" ]; then
162
- $PYTHON_CMD -c "
163
- import json
164
- total_ms = 0
165
- agents = set()
166
- phases = []
167
- with open('$TELEMETRY_DIR/phases.jsonl') as f:
168
- for line in f:
169
- line=line.strip()
170
- if not line: continue
171
- try:
172
- entry=json.loads(line)
173
- total_ms+=entry.get('elapsed_ms',0)
174
- phases.append(entry.get('to',''))
175
- except: pass
176
-
177
- summary = {
178
- 'phases_completed': phases,
179
- 'total_elapsed_ms': total_ms,
180
- 'total_elapsed_s': round(total_ms/1000, 1),
181
- 'updated_at': '$(date -u +%Y-%m-%dT%H:%M:%SZ)'
182
- }
183
- with open('$TELEMETRY_DIR/summary.json', 'w') as f:
184
- json.dump(summary, f, indent=2)
185
- print(f' 📈 Telemetry summary: {len(phases)} phases, {total_ms/1000:.0f}s total')
186
- " 2>/dev/null || true
187
- fi
188
-
189
82
  echo "[opencode-kit] ✅ Post-flight complete."
package/src/telemetry.sh CHANGED
@@ -32,33 +32,49 @@ case "$MODE" in
32
32
  --phases)
33
33
  if [ -f "$TELEMETRY_DIR/phases.jsonl" ]; then
34
34
  echo "Phase transitions:"
35
- cat "$TELEMETRY_DIR/phases.jsonl" | while IFS= read -r line; do
36
- [ -z "$line" ] && continue
37
- FROM=$(echo "$line" | $PYTHON_CMD -c "import sys,json; d=json.load(sys.stdin); print(d.get('from','?'))" 2>/dev/null)
38
- TO=$(echo "$line" | $PYTHON_CMD -c "import sys,json; d=json.load(sys.stdin); print(d.get('to','?'))" 2>/dev/null)
39
- MS=$(echo "$line" | $PYTHON_CMD -c "import sys,json; d=json.load(sys.stdin); print(d.get('elapsed_ms',0))" 2>/dev/null)
40
- printf " %-20s → %-20s %5.1fs\n" "$FROM" "$TO" "$($PYTHON_CMD -c "print($MS/1000)" 2>/dev/null || echo "0.0")"
41
- done
35
+ $PYTHON_CMD -c "
36
+ import sys, json
37
+ for line in sys.stdin:
38
+ line = line.strip()
39
+ if not line:
40
+ continue
41
+ d = json.loads(line)
42
+ from_ = d.get('from', '?')
43
+ to_ = d.get('to', '?')
44
+ ms = d.get('elapsed_ms', 0)
45
+ print(f' {from_:20s} → {to_:20s} {ms/1000:5.1f}s')
46
+ " < "$TELEMETRY_DIR/phases.jsonl" 2>/dev/null || echo -e "${YELLOW}Could not parse phase data${NC}"
42
47
  else
43
48
  echo -e "${YELLOW}No phase data yet.${NC}"
44
49
  fi
45
50
  ;;
46
51
  --summary|*)
47
52
  if [ -f "$TELEMETRY_DIR/summary.json" ]; then
48
- TOTAL_S=$($PYTHON_CMD -c "import json; d=json.load(open('$TELEMETRY_DIR/summary.json')); print(d.get('total_elapsed_s',0))" 2>/dev/null || echo "0")
49
- PHASES=$($PYTHON_CMD -c "import json; d=json.load(open('$TELEMETRY_DIR/summary.json')); print(len(d.get('phases_completed',[])))" 2>/dev/null || echo "0")
53
+ read -r TOTAL_S PHASES < <(TELEMETRY_DIR="$TELEMETRY_DIR" $PYTHON_CMD -c "
54
+ import json, os
55
+ d = json.load(open(os.environ['TELEMETRY_DIR'] + '/summary.json'))
56
+ print(d.get('total_elapsed_s', 0))
57
+ print(len(d.get('phases_completed', [])))
58
+ " 2>/dev/null || echo -e "0\n0")
50
59
  echo " Total elapsed: ${TOTAL_S}s"
51
60
  echo " Phases completed: $PHASES"
52
61
  echo ""
53
62
  echo " Latest phases:"
54
- tail -5 "$TELEMETRY_DIR/phases.jsonl" 2>/dev/null | while IFS= read -r line; do
55
- [ -z "$line" ] && continue
56
- TS=$(echo "$line" | $PYTHON_CMD -c "import sys,json; d=json.load(sys.stdin); print(d.get('ts','?'))" 2>/dev/null | cut -dT -f2 | cut -d. -f1)
57
- FROM=$(echo "$line" | $PYTHON_CMD -c "import sys,json; d=json.load(sys.stdin); print(d.get('from','?'))" 2>/dev/null)
58
- TO=$(echo "$line" | $PYTHON_CMD -c "import sys,json; d=json.load(sys.stdin); print(d.get('to','?'))" 2>/dev/null)
59
- MS=$(echo "$line" | $PYTHON_CMD -c "import sys,json; d=json.load(sys.stdin); print(d.get('elapsed_ms',0))" 2>/dev/null)
60
- echo " $TS $FROM $TO ($((MS/1000))s)"
61
- done
63
+ tail -5 "$TELEMETRY_DIR/phases.jsonl" 2>/dev/null | $PYTHON_CMD -c "
64
+ import sys, json
65
+ for line in sys.stdin:
66
+ line = line.strip()
67
+ if not line:
68
+ continue
69
+ d = json.loads(line)
70
+ ts = d.get('ts', '?')
71
+ if ts != '?':
72
+ ts = ts.split('T')[1].split('.')[0] if 'T' in ts else ts
73
+ from_ = d.get('from', '?')
74
+ to_ = d.get('to', '?')
75
+ ms = d.get('elapsed_ms', 0)
76
+ print(f' {ts} {from_} → {to_} ({ms//1000}s)')
77
+ " 2>/dev/null || echo -e "${YELLOW}Could not parse phase data${NC}"
62
78
  else
63
79
  echo -e "${YELLOW}No telemetry data yet. Run a phase first.${NC}"
64
80
  echo " Phases are recorded automatically by postflight.sh"
package/src/update.sh CHANGED
@@ -28,6 +28,12 @@ while [ $# -gt 0 ]; do
28
28
  esac
29
29
  done
30
30
 
31
+ # --- Verify Python availability ---
32
+ if [ -z "${PYTHON_CMD:-}" ]; then
33
+ echo -e "${RED}❌ PYTHON_CMD is not set. Python is required for update operations.${NC}"
34
+ exit 1
35
+ fi
36
+
31
37
  echo -e "${CYAN}[opencode-kit] 🔄 Update check${NC}"
32
38
  echo " Current dir: $PWD"
33
39
  echo " Source: $REPO_URL (branch: $VERSION)"
@@ -42,6 +48,7 @@ fi
42
48
 
43
49
  # --- Clone latest to temp ---
44
50
  TEMP_DIR=$(mktemp -d /tmp/opencode-kit-XXXXX)
51
+ trap 'rm -rf "$TEMP_DIR"' EXIT INT TERM
45
52
  echo " Cloning latest version to $TEMP_DIR..."
46
53
 
47
54
  if ! git clone --depth 1 --branch "$VERSION" "$REPO_URL" "$TEMP_DIR" 2>/dev/null; then
@@ -62,9 +69,9 @@ print(d.get('contract_version', 'unknown'))
62
69
  " 2>/dev/null || echo "unknown")
63
70
  fi
64
71
 
65
- LATEST_VERSION=$($PYTHON_CMD -c "
66
- import json
67
- with open('$TEMP_DIR/templates/contract.json') as f:
72
+ LATEST_VERSION=$(TEMP_DIR="$TEMP_DIR" $PYTHON_CMD -c "
73
+ import os, json
74
+ with open(os.environ['TEMP_DIR'] + '/templates/contract.json') as f:
68
75
  d=json.load(f)
69
76
  print(d.get('contract_version', 'unknown'))
70
77
  " 2>/dev/null || echo "unknown")
@@ -82,8 +89,8 @@ fi
82
89
  # --- Backup contract state ---
83
90
  echo " Backing up contract state..."
84
91
  STATE_BACKUP=$(mktemp /tmp/opencode-contract-state-XXXXX.json)
85
- $PYTHON_CMD -c "
86
- import json
92
+ STATE_BACKUP="$STATE_BACKUP" $PYTHON_CMD -c "
93
+ import os, json
87
94
  with open('.opencode/orchestration/contract.json') as f:
88
95
  d = json.load(f)
89
96
  # Extract only the state fields to preserve
@@ -98,7 +105,7 @@ state = {
98
105
  'score': d.get('score', {}),
99
106
  'outputs': d.get('outputs', {})
100
107
  }
101
- with open('$STATE_BACKUP', 'w') as f:
108
+ with open(os.environ['STATE_BACKUP'], 'w') as f:
102
109
  json.dump(state, f, indent=2)
103
110
  " 2>/dev/null || echo " ⚠️ Could not backup contract state"
104
111
  echo " ✅ State backed up"
@@ -151,18 +158,18 @@ update_file "$TEMP_DIR/templates/superpowers-contract.json" ".opencode/templates
151
158
 
152
159
  # --- Restore contract state ---
153
160
  if [ "$DRY_RUN" = false ] && [ -f "$STATE_BACKUP" ]; then
154
- $PYTHON_CMD -c "
155
- import json
161
+ STATE_BACKUP="$STATE_BACKUP" LATEST_VERSION="$LATEST_VERSION" $PYTHON_CMD -c "
162
+ import os, json
156
163
  with open('.opencode/orchestration/contract.json') as f:
157
164
  contract = json.load(f)
158
- with open('$STATE_BACKUP') as f:
165
+ with open(os.environ['STATE_BACKUP']) as f:
159
166
  state = json.load(f)
160
167
  # Merge preserved state back into new contract
161
168
  for key, val in state.items():
162
169
  if val: # only overwrite if backup has data
163
170
  contract[key] = val
164
171
  # Update contract_version to latest
165
- contract['contract_version'] = '$LATEST_VERSION'
172
+ contract['contract_version'] = os.environ['LATEST_VERSION']
166
173
  with open('.opencode/orchestration/contract.json', 'w') as f:
167
174
  json.dump(contract, f, indent=2)
168
175
  " 2>/dev/null && echo " ✅ Contract state restored" || echo " ⚠️ Contract state restore failed"