@ikieaneh/opencode-kit 0.6.1 → 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 +101 -84
- package/docs/examples/QUICKSTART.md +1 -1
- package/package.json +1 -1
- package/src/adr.sh +36 -40
- package/src/diff.sh +19 -5
- package/src/init.sh +2 -2
- package/src/telemetry.sh +33 -17
- package/src/update.sh +17 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-kit",
|
|
3
3
|
"description": "Standardized orchestration framework — contract-based, rules-enforced, zero-touch agent workflow",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rizki Rachman",
|
|
7
7
|
"url": "https://github.com/RizkiRachman"
|
|
@@ -73,33 +73,38 @@ const resolveConfigPath = (projectDir, relPath) => {
|
|
|
73
73
|
|
|
74
74
|
// --- Auto-init contract.json if missing ---
|
|
75
75
|
const ensureContract = (projectDir) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
fs.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
76
|
+
try {
|
|
77
|
+
const homeDir = os.homedir();
|
|
78
|
+
const globalDir = path.join(homeDir, '.config/opencode-kit');
|
|
79
|
+
const contractPath = path.join(projectDir, '.opencode', 'orchestration', 'contract.json');
|
|
80
|
+
|
|
81
|
+
// Already exists — nothing to do
|
|
82
|
+
if (fs.existsSync(contractPath)) return contractPath;
|
|
83
|
+
|
|
84
|
+
// Check global config first
|
|
85
|
+
const globalContract = path.join(globalDir, 'orchestration', 'contract.json');
|
|
86
|
+
if (fs.existsSync(globalContract)) {
|
|
87
|
+
fs.mkdirSync(path.dirname(contractPath), { recursive: true });
|
|
88
|
+
fs.copyFileSync(globalContract, contractPath);
|
|
89
|
+
log('info', `Auto-initialized contract from global config: ${contractPath}`);
|
|
90
|
+
return contractPath;
|
|
91
|
+
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
// Scaffold from plugin template
|
|
94
|
+
const templatePath = path.join(TEMPLATES_DIR, 'contract.json');
|
|
95
|
+
if (fs.existsSync(templatePath)) {
|
|
96
|
+
fs.mkdirSync(path.dirname(contractPath), { recursive: true });
|
|
97
|
+
fs.copyFileSync(templatePath, contractPath);
|
|
98
|
+
log('info', `Auto-initialized contract from plugin template: ${contractPath}`);
|
|
99
|
+
return contractPath;
|
|
100
|
+
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
log('warn', 'Could not auto-initialize contract — no template found');
|
|
103
|
+
return null;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
log('error', `Failed to auto-init contract: ${err.message}`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
103
108
|
};
|
|
104
109
|
|
|
105
110
|
// --- Load bootstrap content (cached) ---
|
|
@@ -160,86 +165,98 @@ export const OpencodeKitPlugin = async ({ client, directory }) => {
|
|
|
160
165
|
ensureContract(projectDir);
|
|
161
166
|
|
|
162
167
|
// Ensure global config directory exists
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
try {
|
|
169
|
+
fs.mkdirSync(path.join(globalConfigDir, 'orchestration'), { recursive: true });
|
|
170
|
+
fs.mkdirSync(path.join(globalConfigDir, 'rules'), { recursive: true });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
log('warn', `Failed to create global config dirs: ${err.message}`);
|
|
173
|
+
}
|
|
165
174
|
|
|
166
175
|
return {
|
|
167
176
|
// Skill resolution order (first match wins):
|
|
168
177
|
// 1. .opencode/skills/<name>/ (user project — highest priority)
|
|
169
178
|
// 2. plugin skills/<name>/ (opencode-kit defaults — fallback)
|
|
170
179
|
config: async (config) => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
try {
|
|
181
|
+
config.skills = config.skills || {};
|
|
182
|
+
config.skills.paths = config.skills.paths || [];
|
|
183
|
+
|
|
184
|
+
// Detect if other plugins might conflict with opencode-kit's system prompt
|
|
185
|
+
if (config.plugins && Array.isArray(config.plugins)) {
|
|
186
|
+
const kitIndex = config.plugins.findIndex(p =>
|
|
187
|
+
typeof p === 'string' && p.includes('opencode-kit')
|
|
188
|
+
);
|
|
189
|
+
if (kitIndex > 0) {
|
|
190
|
+
const firstPlugin = config.plugins[0];
|
|
191
|
+
log('warn', `Plugin ordering conflict: opencode-kit should be FIRST, but found '${firstPlugin}' at position 0 and opencode-kit at position ${kitIndex}`);
|
|
192
|
+
}
|
|
182
193
|
}
|
|
183
|
-
}
|
|
184
194
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
// Register user project skills FIRST (higher priority)
|
|
196
|
+
const userSkillsDir = path.join(projectDir, '.opencode/skills');
|
|
197
|
+
if (fs.existsSync(userSkillsDir) && !config.skills.paths.includes(userSkillsDir)) {
|
|
198
|
+
config.skills.paths.push(userSkillsDir);
|
|
199
|
+
log('info', `Registered user skills: ${userSkillsDir}`);
|
|
200
|
+
}
|
|
191
201
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
// Register plugin skills SECOND (fallback)
|
|
203
|
+
if (!config.skills.paths.includes(SKILLS_DIR)) {
|
|
204
|
+
config.skills.paths.push(SKILLS_DIR);
|
|
205
|
+
log('info', `Registered plugin skills: ${SKILLS_DIR}`);
|
|
206
|
+
}
|
|
197
207
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
// Register global config skills path
|
|
209
|
+
if (!config.skills.paths.includes(globalConfigDir)) {
|
|
210
|
+
if (fs.existsSync(globalConfigDir)) {
|
|
211
|
+
config.skills.paths.push(globalConfigDir);
|
|
212
|
+
}
|
|
202
213
|
}
|
|
203
|
-
}
|
|
204
214
|
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
// Provide default contract key hint for agents
|
|
216
|
+
config.contractKey = contractKey;
|
|
217
|
+
} catch (err) {
|
|
218
|
+
log('error', `config hook failed: ${err.message}`);
|
|
219
|
+
}
|
|
207
220
|
},
|
|
208
221
|
|
|
209
222
|
'experimental.chat.messages.transform': async (_input, output) => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
223
|
+
try {
|
|
224
|
+
const bootstrap = getBootstrapContent();
|
|
225
|
+
if (!bootstrap || !output.messages.length) return;
|
|
226
|
+
|
|
227
|
+
// Check contract for rule_overrides and inject them into bootstrap
|
|
228
|
+
const contractPath = path.join(projectDir, '.opencode', 'orchestration', 'contract.json');
|
|
229
|
+
let finalBootstrap = bootstrap;
|
|
230
|
+
if (fs.existsSync(contractPath)) {
|
|
231
|
+
try {
|
|
232
|
+
const contractRaw = fs.readFileSync(contractPath, 'utf8');
|
|
233
|
+
const contract = JSON.parse(contractRaw);
|
|
234
|
+
if (contract.validation && contract.validation.rule_overrides) {
|
|
235
|
+
const overrides = contract.validation.rule_overrides;
|
|
236
|
+
const overrideKeys = Object.keys(overrides);
|
|
237
|
+
if (overrideKeys.length > 0) {
|
|
238
|
+
const overrideText = overrideKeys
|
|
239
|
+
.map(id => ` - ${id}: action → ${overrides[id]}`)
|
|
240
|
+
.join('\n');
|
|
241
|
+
finalBootstrap = bootstrap + `\n## Rule Overrides (from contract)\n\nThe following rule severities have been overridden:\n${overrideText}\n`;
|
|
242
|
+
}
|
|
228
243
|
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
log('warn', `Failed to parse contract for rule_overrides: ${err.message}`);
|
|
229
246
|
}
|
|
230
|
-
} catch (err) {
|
|
231
|
-
log('warn', `Failed to parse contract for rule_overrides: ${err.message}`);
|
|
232
247
|
}
|
|
233
|
-
}
|
|
234
248
|
|
|
235
|
-
|
|
236
|
-
|
|
249
|
+
const firstUser = output.messages.find(m => m.info.role === 'user');
|
|
250
|
+
if (!firstUser || !firstUser.parts.length) return;
|
|
237
251
|
|
|
238
|
-
|
|
239
|
-
|
|
252
|
+
// Guard: skip if already injected
|
|
253
|
+
if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('opencode-kit'))) return;
|
|
240
254
|
|
|
241
|
-
|
|
242
|
-
|
|
255
|
+
const ref = firstUser.parts[0];
|
|
256
|
+
firstUser.parts.unshift({ ...ref, type: 'text', text: finalBootstrap });
|
|
257
|
+
} catch (err) {
|
|
258
|
+
log('error', `messages.transform hook failed: ${err.message}`);
|
|
259
|
+
}
|
|
243
260
|
}
|
|
244
261
|
};
|
|
245
262
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikieaneh/opencode-kit",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Standardized OpenCode orchestration framework — contract-based, rules-enforced, zero-touch agent workflow. Install as plugin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "RizkiRachman",
|
package/src/adr.sh
CHANGED
|
@@ -59,24 +59,31 @@ if [ -z "$TITLE" ]; then
|
|
|
59
59
|
fi
|
|
60
60
|
|
|
61
61
|
# --- Compute next ADR ID ---
|
|
62
|
-
NEXT_ID=$($PYTHON_CMD -c "
|
|
63
|
-
import json
|
|
64
|
-
with open('
|
|
62
|
+
NEXT_ID=$(CONTRACT_FILE="$CONTRACT_FILE" $PYTHON_CMD -c "
|
|
63
|
+
import json, os
|
|
64
|
+
with open(os.environ['CONTRACT_FILE']) as f: d = json.load(f)
|
|
65
65
|
log = d.get('decisions', {}).get('adr_log', [])
|
|
66
66
|
if not log:
|
|
67
67
|
print('ADR-001')
|
|
68
68
|
else:
|
|
69
|
-
last =
|
|
69
|
+
last = 0
|
|
70
|
+
for entry in log:
|
|
71
|
+
try:
|
|
72
|
+
num = int(entry.get('id','ADR-000').replace('ADR-',''))
|
|
73
|
+
if num > last: last = num
|
|
74
|
+
except ValueError:
|
|
75
|
+
continue
|
|
70
76
|
print(f'ADR-{last+1:03d}')
|
|
71
77
|
")
|
|
72
78
|
|
|
73
79
|
# --- Check for duplicate title ---
|
|
74
|
-
DUP=$($PYTHON_CMD -c "
|
|
75
|
-
import json
|
|
76
|
-
with open('
|
|
80
|
+
DUP=$(CONTRACT_FILE="$CONTRACT_FILE" TITLE="$TITLE" $PYTHON_CMD -c "
|
|
81
|
+
import json, os
|
|
82
|
+
with open(os.environ['CONTRACT_FILE']) as f: d = json.load(f)
|
|
77
83
|
log = d.get('decisions', {}).get('adr_log', [])
|
|
84
|
+
search_title = os.environ.get('TITLE', '').lower().strip()
|
|
78
85
|
for entry in log:
|
|
79
|
-
if entry.get('title','').lower().strip() ==
|
|
86
|
+
if entry.get('title','').lower().strip() == search_title:
|
|
80
87
|
print(entry.get('id',''))
|
|
81
88
|
break
|
|
82
89
|
" 2>/dev/null)
|
|
@@ -86,50 +93,37 @@ if [ -n "$DUP" ]; then
|
|
|
86
93
|
exit 0
|
|
87
94
|
fi
|
|
88
95
|
|
|
89
|
-
# --- Build ADR entry via
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
cat > "$ADR_DATA" << ADRDATA
|
|
93
|
-
{
|
|
94
|
-
"title": "$TITLE",
|
|
95
|
-
"context": "$CONTEXT",
|
|
96
|
-
"decision": "$DECISION",
|
|
97
|
-
"alternatives": "$ALTERNATIVES",
|
|
98
|
-
"consequences": "$CONSEQUENCES"
|
|
99
|
-
}
|
|
100
|
-
ADRDATA
|
|
101
|
-
|
|
102
|
-
$PYTHON_CMD -c "
|
|
103
|
-
import json
|
|
96
|
+
# --- Build ADR entry via Python (avoids shell injection) ---
|
|
97
|
+
ENTRY_FILE=$(mktemp /tmp/opencode-adr-entry-XXXXX.json)
|
|
98
|
+
trap 'rm -f "$ENTRY_FILE"' EXIT INT TERM
|
|
104
99
|
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
TITLE="$TITLE" CONTEXT="$CONTEXT" DECISION="$DECISION" ALTERNATIVES="$ALTERNATIVES" CONSEQUENCES="$CONSEQUENCES" NEXT_ID="$NEXT_ID" ENTRY_FILE="$ENTRY_FILE" $PYTHON_CMD -c "
|
|
101
|
+
import json, os
|
|
102
|
+
from datetime import date
|
|
107
103
|
|
|
108
104
|
entry = {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
'id': os.environ['NEXT_ID'],
|
|
106
|
+
'date': date.today().isoformat(),
|
|
107
|
+
'title': os.environ.get('TITLE', ''),
|
|
108
|
+
'context': os.environ.get('CONTEXT', ''),
|
|
109
|
+
'decision': os.environ.get('DECISION', ''),
|
|
110
|
+
'alternatives': os.environ.get('ALTERNATIVES', ''),
|
|
111
|
+
'consequences': os.environ.get('CONSEQUENCES', '')
|
|
116
112
|
}
|
|
117
113
|
|
|
118
|
-
with open(
|
|
114
|
+
with open(os.environ['ENTRY_FILE'], 'w') as f:
|
|
119
115
|
json.dump(entry, f, indent=2)
|
|
120
116
|
print('Entry written')
|
|
121
117
|
"
|
|
122
118
|
|
|
123
|
-
rm -f "$ADR_DATA"
|
|
124
|
-
|
|
125
119
|
# --- Inject into contract.json ---
|
|
126
|
-
$PYTHON_CMD -c "
|
|
127
|
-
import json
|
|
120
|
+
CONTRACT_FILE="$CONTRACT_FILE" ENTRY_FILE="$ENTRY_FILE" $PYTHON_CMD -c "
|
|
121
|
+
import json, os
|
|
128
122
|
|
|
129
|
-
with open('
|
|
123
|
+
with open(os.environ['CONTRACT_FILE']) as f:
|
|
130
124
|
contract = json.load(f)
|
|
131
125
|
|
|
132
|
-
with open(
|
|
126
|
+
with open(os.environ['ENTRY_FILE']) as f:
|
|
133
127
|
entry = json.load(f)
|
|
134
128
|
|
|
135
129
|
if 'decisions' not in contract:
|
|
@@ -139,12 +133,14 @@ if 'adr_log' not in contract['decisions']:
|
|
|
139
133
|
|
|
140
134
|
contract['decisions']['adr_log'].append(entry)
|
|
141
135
|
|
|
142
|
-
with open('
|
|
136
|
+
with open(os.environ['CONTRACT_FILE'], 'w') as f:
|
|
143
137
|
json.dump(contract, f, indent=2)
|
|
144
138
|
|
|
145
139
|
print(json.dumps(entry, indent=2))
|
|
146
140
|
"
|
|
147
141
|
|
|
142
|
+
rm -f "$ENTRY_FILE"
|
|
143
|
+
|
|
148
144
|
echo ""
|
|
149
145
|
echo -e "${GREEN}[opencode-kit] ✅ ADR recorded: $NEXT_ID${NC}"
|
|
150
146
|
echo " Title: $TITLE"
|
package/src/diff.sh
CHANGED
|
@@ -42,13 +42,25 @@ if [ -z "$CONTRACT_A" ] && [ -z "$CONTRACT_B" ]; then
|
|
|
42
42
|
fi
|
|
43
43
|
|
|
44
44
|
# Show state diff
|
|
45
|
+
# Write contract data to temp files for safe Python consumption
|
|
46
|
+
CONTRACT_A_FILE=$(mktemp /tmp/opencode-diff-a-XXXXXX.json 2>/dev/null || mktemp /tmp/opencode-diff-a-XXXXXX)
|
|
47
|
+
CONTRACT_B_FILE=$(mktemp /tmp/opencode-diff-b-XXXXXX.json 2>/dev/null || mktemp /tmp/opencode-diff-b-XXXXXX)
|
|
48
|
+
trap 'rm -f "$CONTRACT_A_FILE" "$CONTRACT_B_FILE"' EXIT INT TERM
|
|
49
|
+
|
|
50
|
+
echo "$CONTRACT_A" > "$CONTRACT_A_FILE"
|
|
51
|
+
echo "$CONTRACT_B" > "$CONTRACT_B_FILE"
|
|
52
|
+
|
|
45
53
|
if [ -n "$PYTHON_CMD" ]; then
|
|
46
54
|
$PYTHON_CMD -c "
|
|
47
55
|
import json, sys
|
|
48
56
|
|
|
49
|
-
def get_state(
|
|
57
|
+
def get_state(filepath):
|
|
50
58
|
try:
|
|
51
|
-
|
|
59
|
+
with open(filepath) as f:
|
|
60
|
+
content = f.read().strip()
|
|
61
|
+
if not content:
|
|
62
|
+
return None
|
|
63
|
+
d = json.loads(content)
|
|
52
64
|
return {
|
|
53
65
|
'state': d.get('state', '?'),
|
|
54
66
|
'goal': (d.get('requirements', {}) or {}).get('goal', '?'),
|
|
@@ -60,12 +72,12 @@ def get_state(c):
|
|
|
60
72
|
except:
|
|
61
73
|
return None
|
|
62
74
|
|
|
63
|
-
a = get_state('
|
|
64
|
-
b = get_state('
|
|
75
|
+
a = get_state('$CONTRACT_A_FILE')
|
|
76
|
+
b = get_state('$CONTRACT_B_FILE')
|
|
65
77
|
|
|
66
78
|
if a and b:
|
|
67
79
|
print(f' Field $BRANCH_A $BRANCH_B')
|
|
68
|
-
print(f' {"
|
|
80
|
+
print(f' {\"-\"*50}')
|
|
69
81
|
for field in ['state', 'goal', 'score', 'version']:
|
|
70
82
|
va = str(a.get(field, '?'))[:20]
|
|
71
83
|
vb = str(b.get(field, '?'))[:20]
|
|
@@ -80,6 +92,8 @@ elif b and not a:
|
|
|
80
92
|
print(f' Contract exists in $BRANCH_B but NOT in $BRANCH_A')
|
|
81
93
|
print(f' State: {b.get(\"state\",\"?\")}')
|
|
82
94
|
" 2>/dev/null || echo -e "${YELLOW}Could not parse contract JSON${NC}"
|
|
95
|
+
|
|
96
|
+
rm -f "$CONTRACT_A_FILE" "$CONTRACT_B_FILE"
|
|
83
97
|
fi
|
|
84
98
|
|
|
85
99
|
# Raw git diff
|
package/src/init.sh
CHANGED
|
@@ -83,8 +83,8 @@ if [ -d ".opencode" ]; then
|
|
|
83
83
|
echo " ✅ Backed up to $BACKUP"
|
|
84
84
|
else
|
|
85
85
|
echo ""
|
|
86
|
-
echo -e "${YELLOW}⚠️ .opencode/ already exists. Use --force to
|
|
87
|
-
echo "
|
|
86
|
+
echo -e "${YELLOW}⚠️ .opencode/ already exists. Use --force to re-scaffold (backup + clean).${NC}"
|
|
87
|
+
echo " Skipping — existing .opencode/ preserved."
|
|
88
88
|
fi
|
|
89
89
|
fi
|
|
90
90
|
|
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"
|