@ikieaneh/opencode-kit 0.6.0 → 0.6.1

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.
@@ -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.0",
4
+ "version": "0.6.1",
5
5
  "author": {
6
6
  "name": "Rizki Rachman",
7
7
  "url": "https://github.com/RizkiRachman"
@@ -210,6 +210,28 @@ export const OpencodeKitPlugin = async ({ client, directory }) => {
210
210
  const bootstrap = getBootstrapContent();
211
211
  if (!bootstrap || !output.messages.length) return;
212
212
 
213
+ // Check contract for rule_overrides and inject them into bootstrap
214
+ const contractPath = path.join(projectDir, '.opencode', 'orchestration', 'contract.json');
215
+ let finalBootstrap = bootstrap;
216
+ if (fs.existsSync(contractPath)) {
217
+ try {
218
+ const contractRaw = fs.readFileSync(contractPath, 'utf8');
219
+ const contract = JSON.parse(contractRaw);
220
+ if (contract.validation && contract.validation.rule_overrides) {
221
+ const overrides = contract.validation.rule_overrides;
222
+ const overrideKeys = Object.keys(overrides);
223
+ if (overrideKeys.length > 0) {
224
+ const overrideText = overrideKeys
225
+ .map(id => ` - ${id}: action → ${overrides[id]}`)
226
+ .join('\n');
227
+ finalBootstrap = bootstrap + `\n## Rule Overrides (from contract)\n\nThe following rule severities have been overridden:\n${overrideText}\n`;
228
+ }
229
+ }
230
+ } catch (err) {
231
+ log('warn', `Failed to parse contract for rule_overrides: ${err.message}`);
232
+ }
233
+ }
234
+
213
235
  const firstUser = output.messages.find(m => m.info.role === 'user');
214
236
  if (!firstUser || !firstUser.parts.length) return;
215
237
 
@@ -217,7 +239,7 @@ export const OpencodeKitPlugin = async ({ client, directory }) => {
217
239
  if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('opencode-kit'))) return;
218
240
 
219
241
  const ref = firstUser.parts[0];
220
- firstUser.parts.unshift({ ...ref, type: 'text', text: bootstrap });
242
+ firstUser.parts.unshift({ ...ref, type: 'text', text: finalBootstrap });
221
243
  }
222
244
  };
223
245
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikieaneh/opencode-kit",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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/cli.js CHANGED
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * opencode-kit CLI — version and help
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] Scaffold orchestration framework into project
31
- update [--dry-run] Pull latest templates from GitHub
32
- --version, -v Print version
33
- --help, -h Print this help
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/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
- # === 3. MCP checks ===
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
@@ -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..."
@@ -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."
@@ -0,0 +1,357 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://opencode.kit/schemas/contract.schema.json",
4
+ "title": "OpenCode Orchestration Contract Schema",
5
+ "description": "Validates the orchestration contract envelope used by OpenCode Kit agents for multi-agent coordination and state persistence.",
6
+ "type": "object",
7
+
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string",
11
+ "const": "http://json-schema.org/draft-07/schema#",
12
+ "description": "JSON Schema dialect identifier"
13
+ },
14
+
15
+ "state": {
16
+ "type": "string",
17
+ "enum": ["INIT", "PLAN", "PLAN_SCORED", "EXECUTE", "EXECUTE_SCORED", "REVIEW", "REVIEW_SCORED", "COMPLETE", "BLOCKED"],
18
+ "description": "Current lifecycle state of the contract"
19
+ },
20
+
21
+ "contract_version": {
22
+ "type": "string",
23
+ "description": "Semantic version of the contract schema"
24
+ },
25
+
26
+ "session": {
27
+ "type": "object",
28
+ "properties": {
29
+ "task_id": { "type": "string" },
30
+ "branch": { "type": "string" },
31
+ "created_at": { "type": "string" }
32
+ },
33
+ "additionalProperties": true,
34
+ "description": "Session metadata for traceability and resumption"
35
+ },
36
+
37
+ "scope": {
38
+ "type": "object",
39
+ "properties": {
40
+ "included": { "type": "array", "items": { "type": "string" } },
41
+ "excluded": { "type": "array", "items": { "type": "string" } },
42
+ "boundary": { "type": "string" },
43
+ "parallel_eligible": { "type": "boolean" },
44
+ "max_parallel_agents": { "type": "integer", "minimum": 1 }
45
+ },
46
+ "additionalProperties": true,
47
+ "description": "Work boundaries and parallelism controls"
48
+ },
49
+
50
+ "requirements": {
51
+ "type": "object",
52
+ "properties": {
53
+ "goal": {
54
+ "type": "string",
55
+ "minLength": 1,
56
+ "description": "Primary objective of the task"
57
+ },
58
+ "scope": {
59
+ "type": "object",
60
+ "properties": {
61
+ "included": {
62
+ "type": "array",
63
+ "items": { "type": "string" },
64
+ "description": "In-scope files, directories, or concerns"
65
+ },
66
+ "excluded": {
67
+ "type": "array",
68
+ "items": { "type": "string" },
69
+ "description": "Explicitly out-of-scope items"
70
+ },
71
+ "parallel_eligible": {
72
+ "type": "boolean",
73
+ "description": "Whether this goal supports parallel agent dispatch"
74
+ }
75
+ }
76
+ },
77
+ "success_criteria": {
78
+ "type": "array",
79
+ "items": { "type": "string" },
80
+ "description": "Measurable outcomes that define completion"
81
+ },
82
+ "acceptance_criteria": {
83
+ "type": "array",
84
+ "items": { "type": "string" }
85
+ },
86
+ "constraints": {
87
+ "type": "object",
88
+ "additionalProperties": true,
89
+ "description": "Constraints and guardrails (may be object or array)"
90
+ }
91
+ },
92
+ "required": ["goal"],
93
+ "additionalProperties": true,
94
+ "description": "Task requirements, scope, and definition of done"
95
+ },
96
+
97
+ "decisions": {
98
+ "type": "object",
99
+ "properties": {
100
+ "approved_architecture": {
101
+ "anyOf": [
102
+ { "type": "string" },
103
+ { "type": "object" }
104
+ ],
105
+ "description": "Approved architecture decision or reference"
106
+ },
107
+ "coding_standard": {
108
+ "type": "array",
109
+ "items": { "type": "string" }
110
+ },
111
+ "rejected_approaches": {
112
+ "type": "array",
113
+ "items": { "type": "string" }
114
+ },
115
+ "adr_log": {
116
+ "type": "array",
117
+ "items": {
118
+ "type": "object",
119
+ "properties": {
120
+ "id": { "type": "string" },
121
+ "title": { "type": "string" },
122
+ "status": { "type": "string" },
123
+ "date": { "type": "string" }
124
+ },
125
+ "additionalProperties": true
126
+ },
127
+ "description": "Architecture Decision Record log"
128
+ }
129
+ },
130
+ "additionalProperties": true,
131
+ "description": "Architectural decisions and rejected alternatives"
132
+ },
133
+
134
+ "governance": {
135
+ "type": "object",
136
+ "properties": {
137
+ "active_agent": { "type": "string" },
138
+ "mode": { "type": "string" },
139
+ "applicable_skills": { "type": "array", "items": { "type": "string" } },
140
+ "current_guidance": {
141
+ "type": "string",
142
+ "description": "Active execution guidance for the current agent"
143
+ },
144
+ "rules_references": {
145
+ "type": "array",
146
+ "items": {
147
+ "type": "object",
148
+ "properties": {
149
+ "source": { "type": "string" },
150
+ "sections": {
151
+ "type": "array",
152
+ "items": { "type": "string" }
153
+ }
154
+ },
155
+ "required": ["source", "sections"]
156
+ },
157
+ "description": "References to rule documents governing execution"
158
+ },
159
+ "extension_skills": {
160
+ "type": "array",
161
+ "items": { "type": "string" },
162
+ "description": "Loaded extension skills for this session"
163
+ },
164
+ "permissions": {
165
+ "type": "object",
166
+ "properties": {
167
+ "do": { "type": "array", "items": { "type": "string" } },
168
+ "dont": { "type": "array", "items": { "type": "string" } }
169
+ },
170
+ "additionalProperties": true
171
+ },
172
+ "previous_blockers": {
173
+ "type": "array",
174
+ "items": { "type": "string" }
175
+ },
176
+ "decisions_log": {
177
+ "type": "array",
178
+ "items": {
179
+ "type": "object",
180
+ "properties": {
181
+ "agent": { "type": "string" },
182
+ "decision": { "type": "string" },
183
+ "timestamp": { "type": "string" }
184
+ },
185
+ "additionalProperties": true
186
+ },
187
+ "description": "Historical log of decisions made during execution"
188
+ }
189
+ },
190
+ "additionalProperties": true,
191
+ "description": "Governance and rule enforcement context"
192
+ },
193
+
194
+ "validation": {
195
+ "type": "object",
196
+ "properties": {
197
+ "block_on": {
198
+ "type": "object",
199
+ "properties": {
200
+ "max_test_failures": { "type": "integer", "minimum": 0 },
201
+ "max_score_drop": { "type": "integer", "minimum": 0, "maximum": 100 },
202
+ "max_compile_errors": { "type": "integer", "minimum": 0 }
203
+ },
204
+ "additionalProperties": true
205
+ },
206
+ "rule_overrides": {
207
+ "type": "object",
208
+ "additionalProperties": true
209
+ }
210
+ },
211
+ "additionalProperties": true,
212
+ "description": "Validation thresholds that trigger BLOCKED state"
213
+ },
214
+
215
+ "score": {
216
+ "type": "object",
217
+ "properties": {
218
+ "rules": {
219
+ "type": "object",
220
+ "properties": {
221
+ "pass": { "type": "integer", "minimum": 0 },
222
+ "fail": { "type": "integer", "minimum": 0 },
223
+ "deduction": { "type": "number" },
224
+ "subtotal": { "type": "number" }
225
+ },
226
+ "description": "Rule-based scoring breakdown"
227
+ },
228
+ "judge": {
229
+ "anyOf": [
230
+ {
231
+ "type": "object",
232
+ "properties": {
233
+ "score": { "type": "integer", "minimum": 0, "maximum": 100 },
234
+ "rationale": { "type": "string" },
235
+ "missing_items": { "type": "array", "items": { "type": "string" } }
236
+ },
237
+ "additionalProperties": true
238
+ },
239
+ { "type": "null" }
240
+ ],
241
+ "description": "LLM judge evaluation (nullable)"
242
+ },
243
+ "combined": {
244
+ "type": "number",
245
+ "minimum": 0,
246
+ "maximum": 100,
247
+ "description": "Combined score (0-100)"
248
+ },
249
+ "verdict": {
250
+ "type": "string",
251
+ "enum": ["INIT", "PASS", "RETRY", "BLOCKED"],
252
+ "description": "Overall scoring verdict"
253
+ }
254
+ },
255
+ "additionalProperties": true,
256
+ "description": "Scoring and evaluation results"
257
+ },
258
+
259
+ "retry": {
260
+ "type": "object",
261
+ "properties": {
262
+ "attempt": {
263
+ "type": "integer",
264
+ "minimum": 0,
265
+ "description": "Current retry attempt number"
266
+ },
267
+ "max_attempts": {
268
+ "type": "integer",
269
+ "minimum": 1,
270
+ "description": "Maximum allowed retry attempts"
271
+ },
272
+ "current_phase": {
273
+ "anyOf": [
274
+ { "type": "string" },
275
+ { "type": "null" }
276
+ ],
277
+ "description": "Phase being retried"
278
+ },
279
+ "score_threshold": {
280
+ "type": "integer",
281
+ "minimum": 0,
282
+ "maximum": 100
283
+ },
284
+ "escalation_threshold": {
285
+ "type": "integer",
286
+ "minimum": 0,
287
+ "maximum": 100
288
+ },
289
+ "issues": {
290
+ "type": "array",
291
+ "items": { "type": "string" },
292
+ "description": "List of issues that triggered retry"
293
+ }
294
+ },
295
+ "additionalProperties": true,
296
+ "description": "Retry policy and issue tracking"
297
+ },
298
+
299
+ "outputs": {
300
+ "type": "object",
301
+ "properties": {
302
+ "plan": { "type": ["object", "null"] },
303
+ "architecture": { "type": ["object", "null"] },
304
+ "code_changes": {
305
+ "type": "array",
306
+ "items": {
307
+ "type": "object",
308
+ "properties": {
309
+ "file": { "type": "string" },
310
+ "change_type": { "type": "string", "enum": ["create", "modify", "delete"] },
311
+ "summary": { "type": "string" }
312
+ },
313
+ "additionalProperties": true
314
+ }
315
+ },
316
+ "test_results": { "type": ["object", "null"] },
317
+ "agent_reports": {
318
+ "type": "array",
319
+ "items": { "type": "object" }
320
+ },
321
+ "score_summary": { "type": ["object", "null"] }
322
+ },
323
+ "additionalProperties": true,
324
+ "description": "Artifacts produced during execution"
325
+ },
326
+
327
+ "metrics": {
328
+ "type": "object",
329
+ "properties": {
330
+ "cost_tokens": { "type": "integer", "minimum": 0 },
331
+ "elapsed_ms": { "type": "integer", "minimum": 0 },
332
+ "agents_used": { "type": "array", "items": { "type": "string" } },
333
+ "phases_completed": { "type": "array", "items": { "type": "string" } }
334
+ },
335
+ "additionalProperties": true,
336
+ "description": "Execution performance metrics"
337
+ },
338
+
339
+ "lessons_learned": {
340
+ "type": "array",
341
+ "items": {
342
+ "type": "object",
343
+ "properties": {
344
+ "category": { "type": "string" },
345
+ "insight": { "type": "string" },
346
+ "recommendation": { "type": "string" }
347
+ },
348
+ "additionalProperties": true
349
+ },
350
+ "description": "Cross-session learning artifacts"
351
+ }
352
+ },
353
+
354
+ "additionalProperties": true,
355
+
356
+ "description": "Top-level fields are all optional to support contract evolution. Use property-specific required constraints for nested mandatory fields."
357
+ }