@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/opencode-kit.js +23 -1
- package/package.json +5 -1
- package/src/cli.js +47 -5
- package/src/doctor.sh +37 -1
- package/src/init.sh +13 -0
- package/src/postflight.py +211 -0
- package/src/postflight.sh +46 -153
- 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.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:
|
|
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.
|
|
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
|
|
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/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
|
@@ -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
|
-
# ---
|
|
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."
|
|
@@ -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
|
+
}
|