@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
|
@@ -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,64 +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
|
-
|
|
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
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
log('warn', `Failed to parse contract for rule_overrides: ${err.message}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
212
248
|
|
|
213
|
-
|
|
214
|
-
|
|
249
|
+
const firstUser = output.messages.find(m => m.info.role === 'user');
|
|
250
|
+
if (!firstUser || !firstUser.parts.length) return;
|
|
215
251
|
|
|
216
|
-
|
|
217
|
-
|
|
252
|
+
// Guard: skip if already injected
|
|
253
|
+
if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('opencode-kit'))) return;
|
|
218
254
|
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
}
|
|
221
260
|
}
|
|
222
261
|
};
|
|
223
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",
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
"type": "git",
|
|
42
42
|
"url": "git+https://github.com/RizkiRachman/opencode-kit.git"
|
|
43
43
|
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"lint": "eslint src/ test/",
|
|
46
|
+
"format": "prettier --check src/ test/"
|
|
47
|
+
},
|
|
44
48
|
"bugs": {
|
|
45
49
|
"url": "https://github.com/RizkiRachman/opencode-kit/issues"
|
|
46
50
|
},
|
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/cli.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* opencode-kit CLI — version and
|
|
3
|
+
* opencode-kit CLI — version, help, and project commands
|
|
4
4
|
* Usage: npx opencode-kit --version
|
|
5
5
|
* npx opencode-kit --help
|
|
6
|
+
* npx opencode-kit doctor
|
|
7
|
+
* npx opencode-kit status
|
|
8
|
+
* npx opencode-kit analytics
|
|
6
9
|
*/
|
|
7
10
|
import { fileURLToPath } from 'url';
|
|
8
11
|
import path from 'path';
|
|
9
12
|
import fs from 'fs';
|
|
13
|
+
import { spawnSync } from 'child_process';
|
|
10
14
|
|
|
11
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
16
|
const pkgPath = path.resolve(__dirname, '../package.json');
|
|
@@ -27,10 +31,13 @@ Usage:
|
|
|
27
31
|
npx opencode-kit [command]
|
|
28
32
|
|
|
29
33
|
Commands:
|
|
30
|
-
init [--force]
|
|
31
|
-
update [--dry-run]
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
init [--force] Scaffold orchestration framework into project
|
|
35
|
+
update [--dry-run] Pull latest templates from GitHub
|
|
36
|
+
doctor Run project health checks
|
|
37
|
+
status Show project status
|
|
38
|
+
analytics Show project analytics
|
|
39
|
+
--version, -v Print version
|
|
40
|
+
--help, -h Print this help
|
|
34
41
|
|
|
35
42
|
Plugin mode:
|
|
36
43
|
Add "opencode-kit" to opencode.json plugin array (FIRST position).
|
|
@@ -45,3 +52,38 @@ Docs: ${pkg.homepage}
|
|
|
45
52
|
`);
|
|
46
53
|
process.exit(0);
|
|
47
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Walk up from startDir to find a directory containing .opencode/
|
|
58
|
+
*/
|
|
59
|
+
function findProjectRoot(startDir) {
|
|
60
|
+
let dir = startDir;
|
|
61
|
+
while (dir !== path.dirname(dir)) {
|
|
62
|
+
if (fs.existsSync(path.join(dir, '.opencode'))) {
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
dir = path.dirname(dir);
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const commands = {
|
|
71
|
+
doctor: '.opencode/src/doctor.sh',
|
|
72
|
+
status: '.opencode/src/status.sh',
|
|
73
|
+
analytics: 'src/analytics.sh',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const command = args[0];
|
|
77
|
+
|
|
78
|
+
if (commands[command]) {
|
|
79
|
+
const projectRoot = findProjectRoot(path.resolve(__dirname, '..'));
|
|
80
|
+
|
|
81
|
+
if (!projectRoot) {
|
|
82
|
+
console.error('Not in an opencode-kit project');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const scriptPath = path.resolve(projectRoot, commands[command]);
|
|
87
|
+
const result = spawnSync('bash', [scriptPath], { stdio: 'inherit', cwd: projectRoot });
|
|
88
|
+
process.exit(result.status);
|
|
89
|
+
}
|
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/doctor.sh
CHANGED
|
@@ -42,6 +42,32 @@ else
|
|
|
42
42
|
fi
|
|
43
43
|
fi
|
|
44
44
|
|
|
45
|
+
# === 1b. Contract schema validation ===
|
|
46
|
+
SCHEMA_FILE=".opencode/templates/contract.schema.json"
|
|
47
|
+
if [ -f "$CONTRACT_FILE" ] && [ -f "$SCHEMA_FILE" ] && [ -n "$PYTHON_CMD" ]; then
|
|
48
|
+
if $PYTHON_CMD -c "
|
|
49
|
+
import json, sys
|
|
50
|
+
with open('$CONTRACT_FILE') as f: contract = json.load(f)
|
|
51
|
+
with open('$SCHEMA_FILE') as f: schema = json.load(f)
|
|
52
|
+
# Basic required-field validation
|
|
53
|
+
errors = []
|
|
54
|
+
if not contract.get('state'): errors.append('Missing state')
|
|
55
|
+
if not contract.get('requirements', {}).get('goal'): errors.append('Missing requirements.goal')
|
|
56
|
+
if contract.get('state') not in ['INIT','PLAN','PLAN_SCORED','EXECUTE','EXECUTE_SCORED','REVIEW','REVIEW_SCORED','COMPLETE','BLOCKED']:
|
|
57
|
+
errors.append('Invalid state')
|
|
58
|
+
if errors:
|
|
59
|
+
print('; '.join(errors))
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
else:
|
|
62
|
+
print('OK')
|
|
63
|
+
" 2>/dev/null; then
|
|
64
|
+
echo -e " ✅ Contract schema valid"
|
|
65
|
+
else
|
|
66
|
+
echo -e " ⚠️ Contract schema issues found (non-blocking)"
|
|
67
|
+
ISSUES=$((ISSUES + 1))
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
45
71
|
# === 2. Rules check ===
|
|
46
72
|
echo -e "${CYAN}[RULES]${NC} Checking rules.json..."
|
|
47
73
|
if [ ! -f "$RULES_FILE" ]; then
|
|
@@ -54,7 +80,7 @@ else
|
|
|
54
80
|
fi
|
|
55
81
|
fi
|
|
56
82
|
|
|
57
|
-
# ===
|
|
83
|
+
# === 3a. MCP CLI checks ===
|
|
58
84
|
echo -e "${CYAN}[MCP]${NC} Checking required MCPs..."
|
|
59
85
|
if [ -f "$RULES_FILE" ] && [ -n "$PYTHON_CMD" ]; then
|
|
60
86
|
$PYTHON_CMD -c "
|
|
@@ -78,6 +104,16 @@ for name, cfg in mcps.items():
|
|
|
78
104
|
" 2>/dev/null || ISSUES=$((ISSUES + 1))
|
|
79
105
|
fi
|
|
80
106
|
|
|
107
|
+
# === 3b. MCP connectivity ===
|
|
108
|
+
echo -e "${CYAN}[MCP_CONNECT]${NC} Testing MCP connectivity..."
|
|
109
|
+
if command -v lean-ctx &>/dev/null; then
|
|
110
|
+
if lean-ctx ctx_knowledge status &>/dev/null 2>&1; then
|
|
111
|
+
echo -e " ✅ lean-ctx MCP: responding"
|
|
112
|
+
else
|
|
113
|
+
echo -e " ⚠️ lean-ctx CLI found but MCP not responding (expected in CI)"
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
|
|
81
117
|
# === 4. Git branch ===
|
|
82
118
|
echo -e "${CYAN}[GIT]${NC} Checking branch..."
|
|
83
119
|
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
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
|
|
|
@@ -168,6 +168,13 @@ if [ "$PLUGIN_MODE" = false ]; then
|
|
|
168
168
|
done
|
|
169
169
|
fi
|
|
170
170
|
|
|
171
|
+
# --- Copy git hooks ---
|
|
172
|
+
if [ -d "$KIT_DIR/.githooks" ]; then
|
|
173
|
+
cp -r "$KIT_DIR/.githooks" .githooks
|
|
174
|
+
chmod +x .githooks/pre-commit .githooks/commit-msg
|
|
175
|
+
echo " ✅ .githooks/ (pre-commit, commit-msg)"
|
|
176
|
+
fi
|
|
177
|
+
|
|
171
178
|
# --- Git ignore .opencode/src (scripts are project-specific) ---
|
|
172
179
|
if [ -f ".gitignore" ]; then
|
|
173
180
|
if ! grep -q ".opencode/src" .gitignore 2>/dev/null; then
|
|
@@ -175,6 +182,12 @@ if [ -f ".gitignore" ]; then
|
|
|
175
182
|
fi
|
|
176
183
|
fi
|
|
177
184
|
|
|
185
|
+
# --- Configure git hooks ---
|
|
186
|
+
if [ -d ".githooks" ]; then
|
|
187
|
+
git config core.hooksPath .githooks
|
|
188
|
+
echo " ✅ Git hooks configured (.githooks/)"
|
|
189
|
+
fi
|
|
190
|
+
|
|
178
191
|
# --- Verify ---
|
|
179
192
|
echo ""
|
|
180
193
|
echo "[opencode-kit] Running verification..."
|