@rockysoft/opencode-hybrid-workflow 0.0.0-stage → 0.3.2-beta.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/LICENSE +21 -0
- package/README.md +163 -3
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/assets/agents/backend-engineer.md +167 -0
- package/assets/agents/code-reviewer.md +129 -0
- package/assets/agents/database-engineer.md +162 -0
- package/assets/agents/devops-engineer.md +115 -0
- package/assets/agents/frontend-engineer.md +38 -0
- package/assets/agents/fullstack-architect.md +43 -0
- package/assets/agents/security-auditor.md +132 -0
- package/assets/agents/technical-writer.md +78 -0
- package/assets/agents/test-engineer.md +154 -0
- package/assets/rules.md +93 -0
- package/assets/skills/frontend-design/INSTALLATION.json +9 -0
- package/assets/skills/frontend-design/LICENSE.txt +177 -0
- package/assets/skills/frontend-design/SKILL.md +78 -0
- package/assets/skills/ui-ux-pro-max/INSTALLATION.json +10 -0
- package/assets/skills/ui-ux-pro-max/LICENSE +21 -0
- package/assets/skills/ui-ux-pro-max/SKILL.md +221 -0
- package/assets/skills/ui-ux-pro-max/data/app-interface.csv +33 -0
- package/assets/skills/ui-ux-pro-max/data/catalog-summary.json +78 -0
- package/assets/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/assets/skills/ui-ux-pro-max/data/colors.csv +193 -0
- package/assets/skills/ui-ux-pro-max/data/data-provenance.json +1348 -0
- package/assets/skills/ui-ux-pro-max/data/google-font-licenses.json +20185 -0
- package/assets/skills/ui-ux-pro-max/data/google-fonts.csv +1935 -0
- package/assets/skills/ui-ux-pro-max/data/icons.csv +106 -0
- package/assets/skills/ui-ux-pro-max/data/landing.csv +35 -0
- package/assets/skills/ui-ux-pro-max/data/motion.csv +18 -0
- package/assets/skills/ui-ux-pro-max/data/phosphor-icons-upstream.json +32582 -0
- package/assets/skills/ui-ux-pro-max/data/products.csv +193 -0
- package/assets/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/angular.csv +51 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/avalonia.csv +57 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +60 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/javafx.csv +76 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/nextjs.csv +62 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +77 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +68 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/react.csv +67 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/shadcn.csv +69 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/svelte.csv +56 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/uno.csv +60 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/uwp.csv +56 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/winui.csv +60 -0
- package/assets/skills/ui-ux-pro-max/data/stacks/wpf.csv +57 -0
- package/assets/skills/ui-ux-pro-max/data/styles.csv +89 -0
- package/assets/skills/ui-ux-pro-max/data/typography.csv +75 -0
- package/assets/skills/ui-ux-pro-max/data/ui-reasoning.csv +193 -0
- package/assets/skills/ui-ux-pro-max/data/ux-guidelines.csv +120 -0
- package/assets/skills/ui-ux-pro-max/references/pro-rules.md +117 -0
- package/assets/skills/ui-ux-pro-max/references/quick-reference.md +256 -0
- package/assets/skills/ui-ux-pro-max/scripts/core.py +993 -0
- package/assets/skills/ui-ux-pro-max/scripts/design_system.py +1668 -0
- package/assets/skills/ui-ux-pro-max/scripts/reasoning_contract.py +123 -0
- package/assets/skills/ui-ux-pro-max/scripts/search.py +177 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-api.json +36 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-catalog.json +33 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-existing.csv +3 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-metadata.json +17 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-overrides.json +7 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/icons-curated.csv +4 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-core.json +23 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-package.json +13 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-exports.json +4 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-package.json +5 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-baseline.json +3366 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-cases.json +111 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-thresholds.json +100 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_catalog_refresh.py +354 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_catalog_summary_line_endings.py +78 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_core.py +343 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_core_data_quality.py +198 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_data_contracts.py +421 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py +233 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_design_system_stack.py +61 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_native_desktop_stack_freshness.py +179 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_relevance_evaluator.py +182 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py +82 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_style_taxonomy.py +175 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_text_layout_resilience.py +120 -0
- package/assets/skills/ui-ux-pro-max/scripts/tests/test_web_stack_freshness.py +177 -0
- package/assets/skills/ui-ux-pro-max/scripts/validate_data.py +1095 -0
- package/assets/skills/web-design-guidelines/INSTALLATION.json +1 -0
- package/assets/skills/web-design-guidelines/SKILL.md +18 -0
- package/cli.mjs +91 -0
- package/core/controller.mjs +367 -0
- package/core/dashboard-state.js +48 -0
- package/core/dashboard.css +2 -0
- package/core/dashboard.html +12 -0
- package/core/dashboard.js +72 -0
- package/core/decision.mjs +22 -0
- package/core/design.mjs +139 -0
- package/core/evidence.mjs +82 -0
- package/core/frontend-policy.mjs +34 -0
- package/core/images.mjs +35 -0
- package/core/monitor-data.mjs +58 -0
- package/core/monitor.mjs +24 -0
- package/core/operation-lock.mjs +82 -0
- package/core/plugin.mjs +91 -0
- package/core/posix-lock.mjs +31 -0
- package/core/posix-lock.py +23 -0
- package/core/pricing.mjs +53 -0
- package/core/process-identity.mjs +19 -0
- package/core/research-tools.mjs +37 -0
- package/core/review-context.mjs +43 -0
- package/core/snapshot.mjs +95 -0
- package/core/telemetry.mjs +107 -0
- package/core/workers.mjs +192 -0
- package/core/workflow-policy.mjs +10 -0
- package/docs/configuration.md +134 -0
- package/docs/images/01-install.png +0 -0
- package/docs/images/02-reviewers.png +0 -0
- package/docs/images/03-connection.png +0 -0
- package/docs/images/04-provider.png +0 -0
- package/docs/images/05-model.png +0 -0
- package/docs/images/06-effort.png +0 -0
- package/docs/images/07-design-lead.png +0 -0
- package/docs/images/08-save.png +0 -0
- package/docs/images/09-settings.png +0 -0
- package/docs/images/10-browser.png +0 -0
- package/docs/images/11-browser-confirm.png +0 -0
- package/docs/images/12-browser-ready.png +0 -0
- package/docs/images/13-tools.png +0 -0
- package/docs/images/14-diagnostics.png +0 -0
- package/docs/images/15-connect-provider.png +0 -0
- package/docs/images/16-reopen.png +0 -0
- package/docs/images/17-implementation.png +0 -0
- package/docs/images/18-cli.png +0 -0
- package/docs/images/19-cli-model.png +0 -0
- package/docs/images/20-cli-effort.png +0 -0
- package/index.js +19 -0
- package/lib/commands.mjs +48 -0
- package/lib/config.mjs +63 -0
- package/lib/install.mjs +111 -0
- package/lib/native-browser.mjs +79 -0
- package/lib/opencode-reviewer.mjs +129 -0
- package/lib/platform.mjs +14 -0
- package/lib/reviewer-browser-proxy.mjs +61 -0
- package/lib/reviewer-browser.mjs +35 -0
- package/lib/reviewers.mjs +11 -0
- package/lib/select-reviewers.mjs +58 -0
- package/lib/settings-rpc.mjs +9 -0
- package/lib/settings-service.mjs +129 -0
- package/package.json +64 -6
- package/sidebar/reader.mjs +80 -0
- package/sidebar/settings.mjs +99 -0
- package/tui.js +303 -0
package/core/workers.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {reviewer} from '../lib/reviewers.mjs';
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { Telemetry, jsonLineReader } from './telemetry.mjs';
|
|
6
|
+
import { findingSchema, isMajor } from './decision.mjs';
|
|
7
|
+
import {processIdentity} from './process-identity.mjs';
|
|
8
|
+
import {commandSpec} from '../lib/commands.mjs';
|
|
9
|
+
import {commandEnvironment} from '../lib/platform.mjs';
|
|
10
|
+
import {prepareBrowser,browserTools,browserInstructions} from '../lib/reviewer-browser.mjs';
|
|
11
|
+
|
|
12
|
+
export const resultSchema = {
|
|
13
|
+
type: 'object', additionalProperties: false,
|
|
14
|
+
properties: {
|
|
15
|
+
verdict: { type: 'string', enum: ['pass', 'revise', 'blocked'] },
|
|
16
|
+
report: { type: 'string' },
|
|
17
|
+
findings: { type: 'array', items: findingSchema },
|
|
18
|
+
}, required: ['verdict', 'report', 'findings'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function workerArgs(who, config, folder, images = [], browser = null) {
|
|
22
|
+
const lane=reviewer(config,who);
|
|
23
|
+
// Attachment count is a transport choice, never an evidence quota. Larger
|
|
24
|
+
// sets stay in the frozen images/ manifest and are opened with view_image.
|
|
25
|
+
const inlineImages = images.length <= 4 && images.join('').length < 2048 ? images : [];
|
|
26
|
+
if (lane.adapter === 'codex-cli') return ['exec', '--json', '--ignore-user-config', '--ignore-rules', '--ephemeral', '--skip-git-repo-check',
|
|
27
|
+
'--sandbox', 'read-only', '-C', folder, '-m', lane.model,
|
|
28
|
+
// User config is intentionally isolated, so Windows' required sandbox
|
|
29
|
+
// implementation must be explicit too. Without it shell reads are denied.
|
|
30
|
+
...(process.platform === 'win32' ? ['-c', 'windows.sandbox="elevated"'] : []),
|
|
31
|
+
'-c', 'approval_policy="never"',
|
|
32
|
+
'-c', `model_reasoning_effort="${lane.effort??'high'}"`, '-c', 'web_search="live"',
|
|
33
|
+
...(browser ? ['-c',`mcp_servers.review_browser.command=${JSON.stringify(browser.command)}`,
|
|
34
|
+
'-c',`mcp_servers.review_browser.args=${JSON.stringify(browser.args)}`,
|
|
35
|
+
'-c','mcp_servers.review_browser.startup_timeout_sec=60',
|
|
36
|
+
'-c','mcp_servers.review_browser.tool_timeout_sec=120',
|
|
37
|
+
'-c','mcp_servers.review_browser.default_tools_approval_mode="approve"',
|
|
38
|
+
'-c',`mcp_servers.review_browser.enabled_tools=${JSON.stringify(browserTools)}`] : []),
|
|
39
|
+
...inlineImages.flatMap(image=>['--image',image]),
|
|
40
|
+
'--output-schema', path.join(folder, 'schema.json'), '-o', path.join(folder, 'result.json'), '-'];
|
|
41
|
+
return ['-p', ...(browser?['--setting-sources','','--disable-slash-commands','--settings','{"disableAllHooks":true,"enabledPlugins":{}}']:['--safe-mode']), '--restricted', '--strict-mcp-config', '--mcp-config', JSON.stringify({mcpServers:browser?{review_browser:browser}:{}}),
|
|
42
|
+
'--no-chrome', '--no-session-persistence', '--permission-mode', 'dontAsk', '--permission-prompts', 'none',
|
|
43
|
+
'--tools', 'Read,Glob,Grep,WebSearch,WebFetch', '--allowedTools', ['Read','Glob','Grep','WebSearch','WebFetch',...(browser?browserTools.map(name=>'mcp__review_browser__'+name):[])].join(','),
|
|
44
|
+
'--model', lane.model, '--effort', lane.effort??'high', '--output-format', 'stream-json', '--verbose',
|
|
45
|
+
'--json-schema', JSON.stringify(resultSchema)];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validateResult(value) {
|
|
49
|
+
if (!value || !['pass', 'revise', 'blocked'].includes(value.verdict) || typeof value.report !== 'string' || !value.report.trim() ||
|
|
50
|
+
!Array.isArray(value.findings)) throw new Error('Invalid reviewer result; checkpoint stays closed.');
|
|
51
|
+
if (Object.keys(value).some(key => !['verdict', 'report', 'findings'].includes(key))) throw new Error('Invalid reviewer result fields.');
|
|
52
|
+
const ids = new Set();
|
|
53
|
+
for (const f of value.findings) {
|
|
54
|
+
if (!f || typeof f !== 'object' || Array.isArray(f) ||
|
|
55
|
+
Object.keys(f).some(key => !findingSchema.required.includes(key)) ||
|
|
56
|
+
findingSchema.required.some(key => typeof f[key] !== 'string' || !f[key].trim()) ||
|
|
57
|
+
!findingSchema.properties.severity.enum.includes(f.severity) ||
|
|
58
|
+
!findingSchema.properties.category.enum.includes(f.category) || ids.has(f.id) ||
|
|
59
|
+
(f.category === 'preference' && isMajor(f))) throw new Error('Invalid structured finding; checkpoint stays closed.');
|
|
60
|
+
ids.add(f.id);
|
|
61
|
+
}
|
|
62
|
+
if ((value.verdict === 'pass' && value.findings.some(isMajor)) ||
|
|
63
|
+
(value.verdict === 'revise' && !value.findings.length) ||
|
|
64
|
+
(value.verdict === 'blocked' && !value.findings.some(isMajor))) throw new Error('Inconsistent reviewer verdict; checkpoint stays closed.');
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function workerEnvironment(source = process.env) {
|
|
69
|
+
const env = {};
|
|
70
|
+
const allowed = /^(PATH|PATHEXT|SYSTEMROOT|WINDIR|COMSPEC|TEMP|TMP|TMPDIR|HOME|XDG_CONFIG_HOME|XDG_DATA_HOME|XDG_STATE_HOME|XDG_CACHE_HOME|USERPROFILE|APPDATA|LOCALAPPDATA|HOMEDRIVE|HOMEPATH|USERNAME|USERDOMAIN|PROGRAMFILES|PROGRAMFILES\(X86\)|PROGRAMW6432|OS|SYSTEMDRIVE|LANG|LC_ALL)$/i;
|
|
71
|
+
for (const [key, value] of Object.entries(source)) if (allowed.test(key)) env[key] = value;
|
|
72
|
+
// Official CLIs retain their normal per-user subscription login; never extract tokens.
|
|
73
|
+
return commandEnvironment(env);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function requireSubscription(who, config, folder, invoke = spawnSync) {
|
|
77
|
+
const lane=reviewer(config,who);
|
|
78
|
+
if(lane.adapter==='opencode')return;
|
|
79
|
+
const executable=lane.executable;
|
|
80
|
+
const args = lane.adapter === 'codex-cli' ? ['login', 'status'] : ['auth', 'status'];
|
|
81
|
+
const spec=invoke===spawnSync?commandSpec(executable):{executable,args:[]};
|
|
82
|
+
const result = invoke(spec.executable, [...spec.args,...args], { cwd: folder, env: workerEnvironment(), shell: false, windowsHide: true, encoding: 'utf8', timeout: 15000 });
|
|
83
|
+
let valid = false;
|
|
84
|
+
if (result.status === 0 && lane.adapter === 'codex-cli') valid = /Logged in using ChatGPT/i.test((result.stdout ?? '') + (result.stderr ?? ''));
|
|
85
|
+
if (result.status === 0 && lane.adapter === 'claude-cli') {
|
|
86
|
+
try { const auth = JSON.parse(result.stdout); valid = auth.loggedIn === true && auth.authMethod === 'claude.ai' && auth.apiProvider === 'firstParty' && !!auth.subscriptionType; } catch {}
|
|
87
|
+
}
|
|
88
|
+
if (!valid) throw new Error(`${who}: subscription authentication could not be confirmed. Sign in through the official CLI; API/unknown authentication is refused.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function runProcess(executable, args, folder, input, timeoutMs, signal, observer = {}) {
|
|
92
|
+
if (signal?.aborted) throw new Error('Cancelled before launch.');
|
|
93
|
+
return await new Promise((resolve, reject) => {
|
|
94
|
+
const spec=commandSpec(executable);
|
|
95
|
+
const child = spawn(spec.executable, [...spec.args,...args], { cwd: folder, env: workerEnvironment(), shell: false, windowsHide: true, detached:process.platform!=='win32', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
96
|
+
child.once('spawn', () => observer.onStart?.(child.pid));
|
|
97
|
+
let output = '', failure, size = 0, reapTimer, settled=false;
|
|
98
|
+
const stop = reason => {
|
|
99
|
+
if(failure)return;
|
|
100
|
+
failure ??= new Error(reason);
|
|
101
|
+
if (process.platform === 'win32' && child.pid && child.exitCode === null) {
|
|
102
|
+
spawnSync(path.join(process.env.SystemRoot ?? 'C:/Windows', 'System32/taskkill.exe'), ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, timeout: 5000 });
|
|
103
|
+
} else if(child.pid) { try {process.kill(-child.pid,'SIGTERM');} catch {child.kill();} }
|
|
104
|
+
// A failed OS termination must not hang the tool forever. Retain an explicit
|
|
105
|
+
// live-worker guard so returning an error never unlocks concurrent writers.
|
|
106
|
+
reapTimer=setTimeout(()=>{
|
|
107
|
+
if(settled)return;settled=true;clearTimeout(timer);signal?.removeEventListener('abort',abort);
|
|
108
|
+
failure.unreapedPid=child.pid;failure.workerIdentity=processIdentity(child.pid);
|
|
109
|
+
// An exited root does not prove its descendants exited: inherited pipes
|
|
110
|
+
// can keep this call open. Never clear this uncertainty by PID lookup.
|
|
111
|
+
failure.descendantsUnconfirmed=true;
|
|
112
|
+
child.stdout.destroy();child.stderr.destroy();child.stdin.destroy();child.unref();reject(failure);
|
|
113
|
+
},5000);
|
|
114
|
+
};
|
|
115
|
+
const abort = () => stop('Review cancelled; checkpoint remains closed.');
|
|
116
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
117
|
+
const timer = Number.isFinite(timeoutMs) && timeoutMs > 0 ? setTimeout(() => stop('Process timed out; checkpoint remains closed.'), timeoutMs) : undefined;
|
|
118
|
+
child.stdout.setEncoding('utf8');
|
|
119
|
+
child.stdout.on('data', data => {
|
|
120
|
+
if (observer.streamOnly) {
|
|
121
|
+
// Reviewer CLI tool reads can be much larger than the final report.
|
|
122
|
+
// Consume all events without retaining the full transcript in RAM.
|
|
123
|
+
output = (output + data).slice(-12000); observer.onData?.(data);
|
|
124
|
+
} else {
|
|
125
|
+
size += Buffer.byteLength(data);
|
|
126
|
+
output += data; observer.onData?.(data);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
child.stderr.setEncoding('utf8');
|
|
130
|
+
child.stderr.on('data', data => { if(observer.captureStderr) output=(output+data).slice(-12000); }); // Premium CLI stderr remains private.
|
|
131
|
+
child.stdin.on('error', () => {});
|
|
132
|
+
child.on('error', e => { failure = e; });
|
|
133
|
+
child.on('close', code => {
|
|
134
|
+
clearTimeout(timer);clearTimeout(reapTimer); signal?.removeEventListener('abort', abort);
|
|
135
|
+
if(settled)return;settled=true;
|
|
136
|
+
if (failure || code !== 0) {
|
|
137
|
+
const error = failure ?? new Error(`Process exited ${code}; no approval recorded.`);
|
|
138
|
+
error.output = output.slice(-12000); reject(error);
|
|
139
|
+
}
|
|
140
|
+
else resolve(output);
|
|
141
|
+
});
|
|
142
|
+
child.stdin.end(input);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function runWorker(who, prompt, config, folder, signal, metadata = {}) {
|
|
147
|
+
const lane=reviewer(config,who);
|
|
148
|
+
if(lane.adapter==='opencode') {
|
|
149
|
+
if(!config.nativeRunner)throw Error('OpenCode reviewer adapter requires the active OpenCode plugin host.');
|
|
150
|
+
return config.nativeRunner(who,prompt,config,folder,signal,metadata);
|
|
151
|
+
}
|
|
152
|
+
const telemetry = new Telemetry(who, config, folder, metadata);
|
|
153
|
+
let envelope;
|
|
154
|
+
const stream = jsonLineReader(event => { telemetry.event(event); if (event.type === 'result') envelope = event; });
|
|
155
|
+
const heartbeat = setInterval(() => telemetry.save(), 5000); heartbeat.unref();
|
|
156
|
+
try {
|
|
157
|
+
if (signal?.aborted) throw new Error('Cancelled before launch.');
|
|
158
|
+
requireSubscription(who, config, folder);
|
|
159
|
+
writeFileSync(path.join(folder, 'schema.json'), JSON.stringify(resultSchema));
|
|
160
|
+
const browser=prepareBrowser(config,folder);
|
|
161
|
+
const args = workerArgs(who, config, folder, metadata.images ?? [],browser);
|
|
162
|
+
if(browser){prompt=browserInstructions+'\n\n'+prompt;telemetry.value.browserAvailable=true;telemetry.save();}
|
|
163
|
+
const executable = lane.executable;
|
|
164
|
+
const stdout = await runProcess(executable, args, folder, prompt, config.reviewTimeoutMs ?? 0, signal, {
|
|
165
|
+
streamOnly: true, onStart: pid => telemetry.start(pid), onData: text => stream.push(text),
|
|
166
|
+
});
|
|
167
|
+
stream.finish();
|
|
168
|
+
let result;
|
|
169
|
+
if (lane.adapter === 'codex-cli') result = JSON.parse(readFileSync(path.join(folder, 'result.json'), 'utf8'));
|
|
170
|
+
else {
|
|
171
|
+
if (!envelope) { try { envelope = JSON.parse(stdout); telemetry.event(envelope); } catch {} }
|
|
172
|
+
if (!envelope) throw new Error('Claude produced no final result event; checkpoint stays closed.');
|
|
173
|
+
if (envelope.is_error || envelope.subtype !== 'success') throw new Error('Claude did not complete successfully; checkpoint stays closed.');
|
|
174
|
+
result = envelope.structured_output;
|
|
175
|
+
if (!result) {
|
|
176
|
+
// Some CLI runs return the schema JSON as the final result string.
|
|
177
|
+
// Accept only exact JSON validated below, never prose or inferred approval.
|
|
178
|
+
try { result = JSON.parse(envelope.result); } catch {}
|
|
179
|
+
if (!result) throw new Error(`Claude returned no schema result (fields: ${Object.keys(envelope).join(',')}; result type: ${typeof envelope.result}); checkpoint stays closed.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
try { validateResult(result); telemetry.end('completed', result.verdict); return result; }
|
|
183
|
+
catch {
|
|
184
|
+
throw new Error(`${who}: invalid reviewer schema (verdict type ${typeof result?.verdict}; report type ${typeof result?.report}; findings count ${Array.isArray(result?.findings) ? result.findings.length : 'not-array'}). Checkpoint stays closed.`);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if(error.unreapedPid){telemetry.value.workerExitUnconfirmed=true;telemetry.value.descendantsUnconfirmed=!!error.descendantsUnconfirmed;}
|
|
188
|
+
stream.finish(); telemetry.end(signal?.aborted ? 'cancelled' : 'failed');
|
|
189
|
+
delete error.output; // Never expose raw verbose CLI event tails to OpenCode logs.
|
|
190
|
+
throw error;
|
|
191
|
+
} finally { clearInterval(heartbeat); }
|
|
192
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Shared workflow guidance for the host agents and isolated reviewer processes.
|
|
2
|
+
export const WORKFLOW_POLICY = `Proportionate hybrid workflow:
|
|
3
|
+
Classify by actual change, not page count or code length. Use mode=ui for a visual redesign of an existing app that preserves architecture, data contracts and behavior: go directly to configured design-lead authoring and reviewer validation, then implement and review. Use mode=feature for new applications, substantive new behavior or architecture changes; investigate and plan those before design. Do not label a redesign feature merely because it is substantial visually. Exact cosmetic changes may use cosmetic; faithful bounded extensions use small with existing/supplied design evidence.
|
|
4
|
+
Decision ownership: the configured premium reviewers own material architecture and structural decisions, including new frameworks/dependencies, data models, shared API contracts, server/client boundaries and substantial changes to the approved plan. The implementation model and its native subagents gather facts, present alternatives and implement the approved decisions; they do not independently approve or replace them. The native fullstack-architect is a read-only researcher supporting premium planning. Route a newly discovered material choice through hybrid_checkpoint replan/plan (or design for UI/UX) before dependent implementation. Mechanical implementation details and faithful small/medium extensions within established patterns do not need a new planning ceremony.
|
|
5
|
+
Design acceptance must be risk-based: list changed component families, affected behavior, representative viewports/states and the concrete risk each check covers. Exercise genuinely distinct responsive layouts, error/interaction paths and print behavior that changed. Reuse shared-component coverage; do not demand the Cartesian product of every state and every width by default. Expand coverage when an observed defect or distinct implementation warrants it. An exhaustive matrix invented by an agent is not an explicit user requirement. If an already-approved contract is excessive, obtain a focused design amendment; do not silently ignore it or falsify evidence.
|
|
6
|
+
Separate product defects, missing evidence and preferences. Medium or higher requires a concrete material consequence or an essential unknown about the affected work. A missing screenshot alone is not medium unless that screenshot is necessary to resolve such a risk. Minor documentation wording, optional polish or preferred aesthetics are low/info. Never lower a real defect to force approval.
|
|
7
|
+
Before re-review, address the entire consolidated blocking checklist. Supply checks linking each finding ID to the actual fix and verification; use resolutions [{id,evidence}] for an explicit checklist. Do not send partial submissions hoping another full review will discover what is still missing. If evidence is unavailable, collect it or state the genuine external blocker. No numeric code-line, screenshot-count, correction-round or lifetime-call quota applies.
|
|
8
|
+
On follow-up, verify all earlier blocking findings, the new delta and affected dependencies. Reuse prior review coverage for unchanged responsibilities; do not retrace unrelated persistence/security paths for an evidence-only update. Full frozen source remains available whenever a correction affects those paths. Both reviewers still return fresh verdicts for this artifact; a stale pass never approves changed work. Continue only with at least one pass and no unresolved medium/high/critical finding from either completed reviewer.
|
|
9
|
+
All supplied screenshots remain available as frozen local files. Large submissions use file delivery, not rejection or silent omission. Inspect the relevant changed pixels and requested states; do not reread unchanged images solely because they remain in the manifest. Keep evidence honest: captions and hashes do not establish visual or behavioral correctness.
|
|
10
|
+
Independent reviews run concurrently; source remains locked until both settle. No automatic model/provider substitution. Preserve current task history and genuine unresolved findings.`;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
Use Hybrid Workflow settings inside OpenCode for routine setup. These details are for advanced configuration and migration.
|
|
4
|
+
|
|
5
|
+
For terminal commands below, use `npx --package @rockysoft/opencode-hybrid-workflow@beta opencode-hybrid` in place of `opencode-hybrid` unless you installed the standalone CLI globally.
|
|
6
|
+
|
|
7
|
+
## Configuration
|
|
8
|
+
|
|
9
|
+
Both CLI and OpenCode-provider reviewers can independently inspect the running
|
|
10
|
+
app with Playwright. Configure this through **Hybrid Workflow settings > Reviewer
|
|
11
|
+
browser**. Installing browser support downloads a pinned Playwright MCP package
|
|
12
|
+
and Firefox into plugin-managed tooling, then enables isolated headless sessions.
|
|
13
|
+
The existing implementation MCP entry is preserved. Existing browser commands can
|
|
14
|
+
also be enabled without downloading replacements.
|
|
15
|
+
|
|
16
|
+
Advanced settings live in `reviewBrowser` in `hybrid/config.json`: `command`,
|
|
17
|
+
`browser` (chrome/msedge/firefox/webkit), and `headless`. The CLI browser commands
|
|
18
|
+
remain available for scripted administration.
|
|
19
|
+
|
|
20
|
+
All reviewer adapters can navigate, inspect DOM, resize, click,
|
|
21
|
+
fill test forms, evaluate page JavaScript, inspect console/network information,
|
|
22
|
+
and capture screenshots. Output goes to that review's `browser-artifacts`; tool
|
|
23
|
+
names are logged in `actions.jsonl`. This is not desktop control or the user's
|
|
24
|
+
signed-in browser. Source edits, server startup, local-file upload and arbitrary
|
|
25
|
+
host-code execution remain unavailable; Codex retains read-only shell inspection
|
|
26
|
+
of evidence, while Claude uses its read tools. Browser interactions can still mutate application
|
|
27
|
+
data: use a disposable test environment and synthetic data. Reviewers are told
|
|
28
|
+
to inspect relevant states directly and distinguish live observations from the
|
|
29
|
+
frozen source artifact. Supply the actual app URL in checkpoint checks and keep
|
|
30
|
+
the development/test server running. Browser access does not authorize a reviewer
|
|
31
|
+
to start/restart the app server.
|
|
32
|
+
|
|
33
|
+
OpenCode-provider reviewers use the internal `hybrid_browser` tool for the same
|
|
34
|
+
isolated Playwright access. Each call has its own browser process, started only
|
|
35
|
+
when needed and closed before the review completes. Browser sessions are never
|
|
36
|
+
shared between reviewers or inherited from the implementation agent. Vision-capable
|
|
37
|
+
models receive actual image pixels; text-only models cannot approve visual evidence.
|
|
38
|
+
|
|
39
|
+
`opencode-hybrid config` prints the configuration location, normally
|
|
40
|
+
`~/.config/opencode/hybrid/config.json`. `OPENCODE_CONFIG_DIR`,
|
|
41
|
+
`OPENCODE_HYBRID_CONFIG` can choose another user-owned location from your launcher.
|
|
42
|
+
Plugin `options.configFile` must match that trusted location; a project cannot
|
|
43
|
+
redirect reviewer executable settings to its own file. The installer supports
|
|
44
|
+
`--config-dir PATH` for a separate profile.
|
|
45
|
+
Do not put credentials in this file; OpenCode or the selected official CLI owns authentication.
|
|
46
|
+
|
|
47
|
+
`opencode-hybrid configure` opens an interactive selector: choose one or two
|
|
48
|
+
reviewers, OpenCode provider or external CLI for each, model, supported variant/CLI effort, and the
|
|
49
|
+
planning/design lead. Either slot can use any supported adapter, including two
|
|
50
|
+
OpenCode models, two CLI models, or one of each. `opencode-hybrid models` lists the
|
|
51
|
+
enabled tool-capable models from your OpenCode provider catalog. Any enabled
|
|
52
|
+
provider is eligible; Copilot is an ordinary catalog provider, not a special adapter. No local model
|
|
53
|
+
server, GPU setup, Strata launcher or model download is managed by this package.
|
|
54
|
+
|
|
55
|
+
Noninteractive examples (use actual IDs returned by `models`):
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
opencode-hybrid configure --reviewer1 codex-cli:gpt-6-astra --reviewer2 claude-cli:opus --variant1 high --variant2 high
|
|
59
|
+
opencode-hybrid configure --reviewer1 opencode:PROVIDER/MODEL --reviewer2 none
|
|
60
|
+
opencode-hybrid configure --reviewer1 opencode:PROVIDER/MODEL --reviewer2 claude-cli:opus --variant1 SUPPORTED_VARIANT
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"version": 1,
|
|
66
|
+
"reviewers": {
|
|
67
|
+
"astra": { "adapter": "codex-cli", "model": "gpt-6-astra", "effort": "high", "executable": "codex" },
|
|
68
|
+
"opus": { "adapter": "claude-cli", "model": "opus", "effort": "high", "executable": "claude" }
|
|
69
|
+
},
|
|
70
|
+
"verification": { "executable": "pnpm", "timeoutMs": 0 },
|
|
71
|
+
"reviewTimeoutMs": 0
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`astra` and `opus` are stable storage-slot IDs, not provider restrictions. Set the
|
|
76
|
+
second slot to `null` for one reviewer. An OpenCode slot has
|
|
77
|
+
`{"adapter":"opencode","providerID":"...","model":"...","variant":"..."}`;
|
|
78
|
+
omit `variant` for the provider default. There is no invented universal effort
|
|
79
|
+
mapping. `designAuthor` selects `astra` or `opus`; default is the last enabled slot.
|
|
80
|
+
Legacy defaults remain Codex/Astra and Claude/Opus, both high effort; first-run
|
|
81
|
+
setup asks for explicit choices and initially highlights OpenCode providers.
|
|
82
|
+
|
|
83
|
+
Native reviewers run in read-only OpenCode child sessions with only frozen
|
|
84
|
+
evidence, public-documentation tools and the optional isolated browser. They cannot edit source, delegate or
|
|
85
|
+
recursively start checkpoints. Image reviews require an image-capable model.
|
|
86
|
+
Unknown/disabled models, unsupported variants and invalid reports fail explicitly;
|
|
87
|
+
there is no provider fallback. Provider context limits still apply.
|
|
88
|
+
|
|
89
|
+
Reviewer assignments are pinned for an active task; changing defaults affects
|
|
90
|
+
the next task without restarting OpenCode. Existing findings are retained. One
|
|
91
|
+
reviewer must pass with no medium-or-higher findings; it does not claim independent
|
|
92
|
+
cross-model agreement. With two, both reports must complete and neither may have
|
|
93
|
+
an unresolved medium-or-higher finding. Low/info suggestions do not block a pass.
|
|
94
|
+
|
|
95
|
+
Installation merges plugin entries into `opencode.json` and `cli.json`, installs
|
|
96
|
+
nine specialist definitions and the three design skills, and adds a managed rules
|
|
97
|
+
file. Existing custom agent/skill files are preserved and reported. Other settings,
|
|
98
|
+
permissions and implementation-model choices are retained. JSONC is accepted;
|
|
99
|
+
comments are removed from the rewritten file, with an exact original backup.
|
|
100
|
+
Updates replace only unchanged package-managed instruction files. It does not
|
|
101
|
+
import personal project memories or modify Claude/Codex global rules.
|
|
102
|
+
|
|
103
|
+
## Storage and existing sessions
|
|
104
|
+
|
|
105
|
+
New state lives outside both the package and project: Windows uses
|
|
106
|
+
`%LOCALAPPDATA%/opencode-hybrid/runtime`, other platforms use
|
|
107
|
+
`$XDG_DATA_HOME/opencode-hybrid/runtime` or `~/.local/share/opencode-hybrid/runtime`.
|
|
108
|
+
Projects are isolated by canonical path. Checkpoints are project-scoped; concurrent
|
|
109
|
+
conversations editing the same project must coordinate against the same gate.
|
|
110
|
+
Opening a subfolder of a Git repository asks you to open its root.
|
|
111
|
+
|
|
112
|
+
To retain a trial project's exact existing history without copying/resetting it:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
opencode-hybrid init --legacy-config /path/to/old/config.json --project /path/to/project
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Only that project receives a runtime override; new projects use portable storage.
|
|
119
|
+
OpenCode conversation history is separate. Sessions created under an isolated
|
|
120
|
+
`XDG_DATA_HOME` remain in that profile; installation does not move them. Existing
|
|
121
|
+
trial profiles stay usable. Restart OpenCode to load changed modules; installation
|
|
122
|
+
does not interrupt running sessions or restart model servers.
|
|
123
|
+
|
|
124
|
+
## Troubleshooting
|
|
125
|
+
|
|
126
|
+
Use **Hybrid Workflow settings > Run diagnostics** to check prerequisites,
|
|
127
|
+
reviewer connections, design skills and browser access. Sign in to each provider
|
|
128
|
+
on the machine where you use OpenCode. Reopen OpenCode after a plugin update.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
The original plugin code is MIT-licensed; bundled skills retain their own
|
|
133
|
+
licenses as listed in `THIRD_PARTY_NOTICES.md`. Authenticate providers on each
|
|
134
|
+
machine rather than copying account credentials.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {resolveNativeGuards} from './core/operation-lock.mjs';
|
|
2
|
+
import {installNativeReviewer} from './lib/opencode-reviewer.mjs';
|
|
3
|
+
import {createPlugin} from './core/plugin.mjs';
|
|
4
|
+
import {readSettings,runtimeFor} from './lib/config.mjs';
|
|
5
|
+
import {registerSettings} from './lib/settings-service.mjs';
|
|
6
|
+
export default {id:'personal.hybrid-checkpoints', async setup(ctx) {
|
|
7
|
+
await registerSettings(ctx,ctx.options);
|
|
8
|
+
const config=readSettings(ctx.options);
|
|
9
|
+
if(!config.enabled)return;
|
|
10
|
+
if(!/^2\./.test(ctx.app?.version ?? '')||typeof ctx.session?.hook!=='function'||typeof ctx.tool?.transform!=='function'||typeof ctx.tool?.hook!=='function')
|
|
11
|
+
throw Error('Hybrid workflow needs OpenCode 2 with session/tool plugin hooks. Run opencode-hybrid doctor.');
|
|
12
|
+
// Validate the actual hook surface, not a pinned patch version.
|
|
13
|
+
config.compatibleOpenCode=[ctx.app.version];
|
|
14
|
+
config.storageFor=directory=>runtimeFor(config,directory);
|
|
15
|
+
config.reloadSettings=()=>readSettings(ctx.options);
|
|
16
|
+
config.nativeRunner=await installNativeReviewer(ctx);
|
|
17
|
+
config.resolveNativeSessions=file=>resolveNativeGuards(file,async sessionID=>{const value=await ctx.session.get({sessionID});return value.data??value;});
|
|
18
|
+
await createPlugin(config).setup(ctx);
|
|
19
|
+
}};
|
package/lib/commands.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {existsSync,readFileSync,readdirSync,statSync,accessSync,constants} from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {commandDirectories} from './platform.mjs';
|
|
4
|
+
// Never invoke a shell or interpolate arguments. Windows package-manager shims
|
|
5
|
+
// are resolved to their literal Node/native target, or rejected with a useful error.
|
|
6
|
+
export function commandSpec(command, env=process.env, platform=process.platform) {
|
|
7
|
+
if(typeof command!=='string'||!command)throw Error('Executable must be a nonempty string.');
|
|
8
|
+
const suffixes=platform==='win32'?['.exe','.cmd','']:[''];
|
|
9
|
+
const dirs=path.isAbsolute(command)?['']:commandDirectories(env,platform);
|
|
10
|
+
let found;
|
|
11
|
+
for(const dir of dirs) {
|
|
12
|
+
for(const suffix of path.extname(command)?['']:suffixes) {
|
|
13
|
+
const file=dir?path.resolve(dir,command+suffix):command+suffix;
|
|
14
|
+
try{if(statSync(file).isFile()){if(platform!=='win32')accessSync(file,constants.X_OK);found=file;break;}}catch{}
|
|
15
|
+
}
|
|
16
|
+
if(found)break;
|
|
17
|
+
}
|
|
18
|
+
if(!found)throw Error(`Executable not found: ${command}. Install it or set its absolute path in hybrid/config.json.`);
|
|
19
|
+
if(platform!=='win32'||!/\.(cmd|bat)$/i.test(found))return {executable:found,args:[]};
|
|
20
|
+
const dir=path.dirname(found),text=readFileSync(found,'utf8');
|
|
21
|
+
// npm/pnpm generated wrappers quote the %dp0% or %~dp0 relative executable.
|
|
22
|
+
const candidates=[...text.matchAll(/"%(?:dp0|~dp0)%?[\\/]([^"\r\n]+\.(?:[cm]?js|exe))"/gi)].map(m=>path.resolve(dir,m[1]));
|
|
23
|
+
// Current Node installers use a variable-based npm wrapper. Resolve its
|
|
24
|
+
// literal bundled CLI assignment, without executing its prefix lookup shell.
|
|
25
|
+
const npmCLI=text.match(/SET "NPM_CLI_JS=%~dp0[\\/]([^"\r\n]*npm-cli\.js)"/i);
|
|
26
|
+
if(npmCLI)candidates.unshift(path.resolve(dir,npmCLI[1]));
|
|
27
|
+
const target=candidates.find(f=>existsSync(f)&&!/node\.exe$/i.test(f));
|
|
28
|
+
if(!target)throw Error(`Cannot safely resolve shim ${found}. Configure the native executable or Node script path.`);
|
|
29
|
+
if(/\.exe$/i.test(target))return {executable:target,args:[]};
|
|
30
|
+
const nodeCandidates=[path.join(dir,'node.exe'),...String(env.PATH??env.Path??'').split(path.delimiter).filter(Boolean).map(p=>path.join(p,'node.exe'))];
|
|
31
|
+
const node=nodeCandidates.find(file=>existsSync(file));
|
|
32
|
+
if(!node)throw Error('Node executable not found for package-manager shim; install Node or add it to PATH.');
|
|
33
|
+
return {executable:node,args:[target]};
|
|
34
|
+
}
|
|
35
|
+
export function nativeCodex(command) {
|
|
36
|
+
const spec=commandSpec(command);
|
|
37
|
+
if(!spec.args.length)return command;
|
|
38
|
+
const script=spec.args[0];
|
|
39
|
+
const modules=path.resolve(script,'../../..');
|
|
40
|
+
const arch=process.arch==='arm64'?'arm64':'x64';
|
|
41
|
+
const platform=process.platform==='win32'?`codex-win32-${arch}`:process.platform==='darwin'?`codex-darwin-${arch}`:`codex-linux-${arch}`;
|
|
42
|
+
const vendor=path.join(modules,'codex','node_modules','@openai',platform,'vendor');
|
|
43
|
+
if(existsSync(vendor))for(const entry of readdirSync(vendor)) {
|
|
44
|
+
const executable=path.join(vendor,entry,'bin',process.platform==='win32'?'codex.exe':'codex');
|
|
45
|
+
if(existsSync(executable))return executable;
|
|
46
|
+
}
|
|
47
|
+
return command;
|
|
48
|
+
}
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {existsSync, readFileSync, realpathSync} from 'node:fs';
|
|
2
|
+
import {homedir} from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import {lanes} from './reviewers.mjs';
|
|
6
|
+
import {validateBrowser} from './reviewer-browser.mjs';
|
|
7
|
+
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
export function configDirectory(env=process.env, home=homedir()) {
|
|
9
|
+
return path.resolve(env.OPENCODE_CONFIG_DIR || path.join(env.XDG_CONFIG_HOME || path.join(home,'.config'),'opencode'));
|
|
10
|
+
}
|
|
11
|
+
export function defaultSettings(env=process.env, home=homedir()) {
|
|
12
|
+
const data = process.platform==='win32' ? (env.LOCALAPPDATA || path.join(home,'AppData','Local')) : (env.XDG_DATA_HOME || path.join(home,'.local','share'));
|
|
13
|
+
return {version:1, enabled:true, runtimeDirectory:path.join(data,'opencode-hybrid','runtime'), projectRuntimeOverrides:{},
|
|
14
|
+
reviewers:{astra:{adapter:'codex-cli',model:'gpt-6-astra',effort:'high',executable:'codex'},opus:{adapter:'claude-cli',model:'opus',effort:'high',executable:'claude'}},
|
|
15
|
+
verification:{executable:'pnpm',timeoutMs:0},reviewTimeoutMs:0,maxPacketChars:400000,
|
|
16
|
+
design:{pythonExecutable:process.platform==='win32'?'python':'python3',skillsDirectory:path.join(packageRoot,'assets','skills')}};
|
|
17
|
+
}
|
|
18
|
+
export function settingsFile(options={},env=process.env) {
|
|
19
|
+
const trusted=env.OPENCODE_HYBRID_CONFIG || path.join(configDirectory(env),'hybrid','config.json');
|
|
20
|
+
const file=options.configFile || trusted;
|
|
21
|
+
if(!path.isAbsolute(file))throw Error('Hybrid configFile must be an absolute path to user-owned configuration.');
|
|
22
|
+
const normalize=value=>process.platform==='win32'?path.resolve(value).toLowerCase():path.resolve(value);
|
|
23
|
+
if(normalize(file)!==normalize(trusted))throw Error('Plugin options cannot select executable settings outside the user config. Use OPENCODE_CONFIG_DIR or OPENCODE_HYBRID_CONFIG from your own launcher.');
|
|
24
|
+
if(!env.OPENCODE_HYBRID_CONFIG&&existsSync(file)) {
|
|
25
|
+
const base=realpathSync(configDirectory(env)),resolved=realpathSync(file),relative=path.relative(base,resolved);
|
|
26
|
+
if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative))throw Error('Hybrid config symlink escapes the user configuration directory.');
|
|
27
|
+
}
|
|
28
|
+
return file;
|
|
29
|
+
}
|
|
30
|
+
export function readSettings(options={},env=process.env) {
|
|
31
|
+
const file=settingsFile(options,env), defaults=defaultSettings(env);
|
|
32
|
+
const supplied=existsSync(file)?JSON.parse(readFileSync(file,'utf8')):{};
|
|
33
|
+
if(supplied.version!==undefined&&supplied.version!==1)throw Error('Unsupported hybrid configuration version.');
|
|
34
|
+
const config={...defaults,...supplied,reviewers:Object.fromEntries(['astra','opus'].map(who=>[who,supplied.reviewers?.[who]===null?null:supplied.reviewers?.[who]?.adapter&&supplied.reviewers[who].adapter!==defaults.reviewers[who].adapter?supplied.reviewers[who]:{...defaults.reviewers[who],...supplied.reviewers?.[who]}])),
|
|
35
|
+
design:{...defaults.design,...supplied.design},verification:{...defaults.verification,...supplied.verification}};
|
|
36
|
+
if(options.enabled===false)config.enabled=false;
|
|
37
|
+
validateBrowser(config.reviewBrowser);
|
|
38
|
+
for(const who of ['astra','opus']) {
|
|
39
|
+
if(supplied.reviewers?.[who]===null)config.reviewers[who]=null;
|
|
40
|
+
const lane=config.reviewers[who];
|
|
41
|
+
if(lane===null||lane.enabled===false)continue;
|
|
42
|
+
if(!['codex-cli','claude-cli','opencode'].includes(lane.adapter))throw Error(`Unsupported reviewer adapter: ${lane.adapter}`);
|
|
43
|
+
for(const key of lane.adapter==='opencode'?['model','providerID']:['model','executable'])if(typeof lane[key]!=='string'||!lane[key].trim())throw Error(`reviewers.${who}.${key} is required.`);
|
|
44
|
+
if(lane.effort && !/^[a-zA-Z0-9_-]+$/.test(lane.effort))throw Error('Invalid reasoning effort.');
|
|
45
|
+
}
|
|
46
|
+
if(!lanes(config).includes('astra'))throw Error('Reviewer 1 (astra storage slot) is required; disable reviewer 2 for single-reviewer mode.');
|
|
47
|
+
if(config.designAuthor && !lanes(config).includes(config.designAuthor))throw Error('designAuthor must name an enabled reviewer slot.');
|
|
48
|
+
if(!path.isAbsolute(config.runtimeDirectory))throw Error('runtimeDirectory must be absolute.');
|
|
49
|
+
for(const [project,root] of Object.entries(config.projectRuntimeOverrides))if(!path.isAbsolute(project)||typeof root!=='string'||!path.isAbsolute(root))throw Error('Project/runtime overrides must use absolute paths.');
|
|
50
|
+
for(const n of [config.reviewTimeoutMs,config.verification.timeoutMs])if(!Number.isFinite(n)||n<0)throw Error('Timeout must be zero (disabled) or a positive duration.');
|
|
51
|
+
return {...config,configFile:file, codex:config.reviewers.astra,claude:config.reviewers.opus,pnpmExecutable:config.verification.executable,verificationTimeoutMs:config.verification.timeoutMs};
|
|
52
|
+
}
|
|
53
|
+
export function canonicalProject(directory) {
|
|
54
|
+
const value=realpathSync(directory);
|
|
55
|
+
return process.platform==='win32'?value.toLowerCase():value;
|
|
56
|
+
}
|
|
57
|
+
export function runtimeFor(config,directory) {
|
|
58
|
+
const project=canonicalProject(directory);
|
|
59
|
+
for(const [key,runtime] of Object.entries(config.projectRuntimeOverrides ?? {})) {
|
|
60
|
+
try {if(canonicalProject(key)===project)return runtime;}catch { /* An absent old project does not affect other projects. */ }
|
|
61
|
+
}
|
|
62
|
+
return config.runtimeDirectory;
|
|
63
|
+
}
|