@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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +163 -3
  3. package/THIRD_PARTY_NOTICES.md +18 -0
  4. package/assets/agents/backend-engineer.md +167 -0
  5. package/assets/agents/code-reviewer.md +129 -0
  6. package/assets/agents/database-engineer.md +162 -0
  7. package/assets/agents/devops-engineer.md +115 -0
  8. package/assets/agents/frontend-engineer.md +38 -0
  9. package/assets/agents/fullstack-architect.md +43 -0
  10. package/assets/agents/security-auditor.md +132 -0
  11. package/assets/agents/technical-writer.md +78 -0
  12. package/assets/agents/test-engineer.md +154 -0
  13. package/assets/rules.md +93 -0
  14. package/assets/skills/frontend-design/INSTALLATION.json +9 -0
  15. package/assets/skills/frontend-design/LICENSE.txt +177 -0
  16. package/assets/skills/frontend-design/SKILL.md +78 -0
  17. package/assets/skills/ui-ux-pro-max/INSTALLATION.json +10 -0
  18. package/assets/skills/ui-ux-pro-max/LICENSE +21 -0
  19. package/assets/skills/ui-ux-pro-max/SKILL.md +221 -0
  20. package/assets/skills/ui-ux-pro-max/data/app-interface.csv +33 -0
  21. package/assets/skills/ui-ux-pro-max/data/catalog-summary.json +78 -0
  22. package/assets/skills/ui-ux-pro-max/data/charts.csv +26 -0
  23. package/assets/skills/ui-ux-pro-max/data/colors.csv +193 -0
  24. package/assets/skills/ui-ux-pro-max/data/data-provenance.json +1348 -0
  25. package/assets/skills/ui-ux-pro-max/data/google-font-licenses.json +20185 -0
  26. package/assets/skills/ui-ux-pro-max/data/google-fonts.csv +1935 -0
  27. package/assets/skills/ui-ux-pro-max/data/icons.csv +106 -0
  28. package/assets/skills/ui-ux-pro-max/data/landing.csv +35 -0
  29. package/assets/skills/ui-ux-pro-max/data/motion.csv +18 -0
  30. package/assets/skills/ui-ux-pro-max/data/phosphor-icons-upstream.json +32582 -0
  31. package/assets/skills/ui-ux-pro-max/data/products.csv +193 -0
  32. package/assets/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  33. package/assets/skills/ui-ux-pro-max/data/stacks/angular.csv +51 -0
  34. package/assets/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  35. package/assets/skills/ui-ux-pro-max/data/stacks/avalonia.csv +57 -0
  36. package/assets/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  37. package/assets/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +60 -0
  38. package/assets/skills/ui-ux-pro-max/data/stacks/javafx.csv +76 -0
  39. package/assets/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  40. package/assets/skills/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
  41. package/assets/skills/ui-ux-pro-max/data/stacks/nextjs.csv +62 -0
  42. package/assets/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +77 -0
  43. package/assets/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +68 -0
  44. package/assets/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  45. package/assets/skills/ui-ux-pro-max/data/stacks/react.csv +67 -0
  46. package/assets/skills/ui-ux-pro-max/data/stacks/shadcn.csv +69 -0
  47. package/assets/skills/ui-ux-pro-max/data/stacks/svelte.csv +56 -0
  48. package/assets/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  49. package/assets/skills/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
  50. package/assets/skills/ui-ux-pro-max/data/stacks/uno.csv +60 -0
  51. package/assets/skills/ui-ux-pro-max/data/stacks/uwp.csv +56 -0
  52. package/assets/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  53. package/assets/skills/ui-ux-pro-max/data/stacks/winui.csv +60 -0
  54. package/assets/skills/ui-ux-pro-max/data/stacks/wpf.csv +57 -0
  55. package/assets/skills/ui-ux-pro-max/data/styles.csv +89 -0
  56. package/assets/skills/ui-ux-pro-max/data/typography.csv +75 -0
  57. package/assets/skills/ui-ux-pro-max/data/ui-reasoning.csv +193 -0
  58. package/assets/skills/ui-ux-pro-max/data/ux-guidelines.csv +120 -0
  59. package/assets/skills/ui-ux-pro-max/references/pro-rules.md +117 -0
  60. package/assets/skills/ui-ux-pro-max/references/quick-reference.md +256 -0
  61. package/assets/skills/ui-ux-pro-max/scripts/core.py +993 -0
  62. package/assets/skills/ui-ux-pro-max/scripts/design_system.py +1668 -0
  63. package/assets/skills/ui-ux-pro-max/scripts/reasoning_contract.py +123 -0
  64. package/assets/skills/ui-ux-pro-max/scripts/search.py +177 -0
  65. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-api.json +36 -0
  66. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-catalog.json +33 -0
  67. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-existing.csv +3 -0
  68. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-metadata.json +17 -0
  69. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-overrides.json +7 -0
  70. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/icons-curated.csv +4 -0
  71. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-core.json +23 -0
  72. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-package.json +13 -0
  73. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-exports.json +4 -0
  74. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-package.json +5 -0
  75. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-baseline.json +3366 -0
  76. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-cases.json +111 -0
  77. package/assets/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-thresholds.json +100 -0
  78. package/assets/skills/ui-ux-pro-max/scripts/tests/test_catalog_refresh.py +354 -0
  79. package/assets/skills/ui-ux-pro-max/scripts/tests/test_catalog_summary_line_endings.py +78 -0
  80. package/assets/skills/ui-ux-pro-max/scripts/tests/test_core.py +343 -0
  81. package/assets/skills/ui-ux-pro-max/scripts/tests/test_core_data_quality.py +198 -0
  82. package/assets/skills/ui-ux-pro-max/scripts/tests/test_data_contracts.py +421 -0
  83. package/assets/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py +233 -0
  84. package/assets/skills/ui-ux-pro-max/scripts/tests/test_design_system_stack.py +61 -0
  85. package/assets/skills/ui-ux-pro-max/scripts/tests/test_native_desktop_stack_freshness.py +179 -0
  86. package/assets/skills/ui-ux-pro-max/scripts/tests/test_relevance_evaluator.py +182 -0
  87. package/assets/skills/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py +82 -0
  88. package/assets/skills/ui-ux-pro-max/scripts/tests/test_style_taxonomy.py +175 -0
  89. package/assets/skills/ui-ux-pro-max/scripts/tests/test_text_layout_resilience.py +120 -0
  90. package/assets/skills/ui-ux-pro-max/scripts/tests/test_web_stack_freshness.py +177 -0
  91. package/assets/skills/ui-ux-pro-max/scripts/validate_data.py +1095 -0
  92. package/assets/skills/web-design-guidelines/INSTALLATION.json +1 -0
  93. package/assets/skills/web-design-guidelines/SKILL.md +18 -0
  94. package/cli.mjs +91 -0
  95. package/core/controller.mjs +367 -0
  96. package/core/dashboard-state.js +48 -0
  97. package/core/dashboard.css +2 -0
  98. package/core/dashboard.html +12 -0
  99. package/core/dashboard.js +72 -0
  100. package/core/decision.mjs +22 -0
  101. package/core/design.mjs +139 -0
  102. package/core/evidence.mjs +82 -0
  103. package/core/frontend-policy.mjs +34 -0
  104. package/core/images.mjs +35 -0
  105. package/core/monitor-data.mjs +58 -0
  106. package/core/monitor.mjs +24 -0
  107. package/core/operation-lock.mjs +82 -0
  108. package/core/plugin.mjs +91 -0
  109. package/core/posix-lock.mjs +31 -0
  110. package/core/posix-lock.py +23 -0
  111. package/core/pricing.mjs +53 -0
  112. package/core/process-identity.mjs +19 -0
  113. package/core/research-tools.mjs +37 -0
  114. package/core/review-context.mjs +43 -0
  115. package/core/snapshot.mjs +95 -0
  116. package/core/telemetry.mjs +107 -0
  117. package/core/workers.mjs +192 -0
  118. package/core/workflow-policy.mjs +10 -0
  119. package/docs/configuration.md +134 -0
  120. package/docs/images/01-install.png +0 -0
  121. package/docs/images/02-reviewers.png +0 -0
  122. package/docs/images/03-connection.png +0 -0
  123. package/docs/images/04-provider.png +0 -0
  124. package/docs/images/05-model.png +0 -0
  125. package/docs/images/06-effort.png +0 -0
  126. package/docs/images/07-design-lead.png +0 -0
  127. package/docs/images/08-save.png +0 -0
  128. package/docs/images/09-settings.png +0 -0
  129. package/docs/images/10-browser.png +0 -0
  130. package/docs/images/11-browser-confirm.png +0 -0
  131. package/docs/images/12-browser-ready.png +0 -0
  132. package/docs/images/13-tools.png +0 -0
  133. package/docs/images/14-diagnostics.png +0 -0
  134. package/docs/images/15-connect-provider.png +0 -0
  135. package/docs/images/16-reopen.png +0 -0
  136. package/docs/images/17-implementation.png +0 -0
  137. package/docs/images/18-cli.png +0 -0
  138. package/docs/images/19-cli-model.png +0 -0
  139. package/docs/images/20-cli-effort.png +0 -0
  140. package/index.js +19 -0
  141. package/lib/commands.mjs +48 -0
  142. package/lib/config.mjs +63 -0
  143. package/lib/install.mjs +111 -0
  144. package/lib/native-browser.mjs +79 -0
  145. package/lib/opencode-reviewer.mjs +129 -0
  146. package/lib/platform.mjs +14 -0
  147. package/lib/reviewer-browser-proxy.mjs +61 -0
  148. package/lib/reviewer-browser.mjs +35 -0
  149. package/lib/reviewers.mjs +11 -0
  150. package/lib/select-reviewers.mjs +58 -0
  151. package/lib/settings-rpc.mjs +9 -0
  152. package/lib/settings-service.mjs +129 -0
  153. package/package.json +64 -6
  154. package/sidebar/reader.mjs +80 -0
  155. package/sidebar/settings.mjs +99 -0
  156. package/tui.js +303 -0
@@ -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
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
+ }};
@@ -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
+ }