@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/opencode-kit.js +104 -65
- package/docs/examples/QUICKSTART.md +1 -1
- package/package.json +5 -1
- package/src/adr.sh +36 -40
- package/src/cli.js +47 -5
- package/src/diff.sh +19 -5
- package/src/doctor.sh +37 -1
- package/src/init.sh +15 -2
- package/src/postflight.py +211 -0
- package/src/postflight.sh +46 -153
- package/src/telemetry.sh +33 -17
- package/src/update.sh +17 -10
- package/templates/contract.schema.json +357 -0
|
@@ -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
|
-
# ---
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
# ---
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
# ---
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
-
# ---
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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 |
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
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'] = '
|
|
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"
|