open-tui-orchestrator 0.9.6

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 (93) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/INSTALL-zh.md +96 -0
  3. package/INSTALL.md +96 -0
  4. package/LICENSE +48 -0
  5. package/README-zh.md +181 -0
  6. package/README.md +181 -0
  7. package/cli.mjs +37 -0
  8. package/docs/adapt.md +103 -0
  9. package/docs/assets/kimicode-agent-swarm-10-subagents.png +0 -0
  10. package/docs/auto-recovery.md +23 -0
  11. package/docs/caller-driven.md +121 -0
  12. package/docs/claude-adapter.md +25 -0
  13. package/docs/execution-contract.md +70 -0
  14. package/docs/inactive-windows.md +11 -0
  15. package/docs/kimi-adapter.md +27 -0
  16. package/docs/kimi-integration.md +56 -0
  17. package/docs/maintenance-lock.md +32 -0
  18. package/docs/openclaw-adapter.md +59 -0
  19. package/docs/openclaw-assessment-2026-09-06.md +59 -0
  20. package/docs/opencode-adapter.md +25 -0
  21. package/docs/pi-adapter.md +58 -0
  22. package/docs/public-readiness.md +63 -0
  23. package/docs/release-policy.md +39 -0
  24. package/docs/security-audit-2026-09-09.md +41 -0
  25. package/docs/trust-and-safety.md +64 -0
  26. package/docs/verification-2026-09-06.md +22 -0
  27. package/docs/verification-recovery-2026-09-06.md +36 -0
  28. package/orch.mjs +20 -0
  29. package/package.json +36 -0
  30. package/release.json +116 -0
  31. package/repair.mjs +228 -0
  32. package/scripts/adapt.mjs +35 -0
  33. package/scripts/agent-auth-prompt.txt +10 -0
  34. package/scripts/agent.mjs +1 -0
  35. package/scripts/core/adapt-lib.mjs +219 -0
  36. package/scripts/core/agent-auth-prompt.txt +10 -0
  37. package/scripts/core/agent-profiles/hermes.json +59 -0
  38. package/scripts/core/agent.mjs +1 -0
  39. package/scripts/core/checkpoint.mjs +38 -0
  40. package/scripts/core/claude-host.mjs +50 -0
  41. package/scripts/core/claude-runtime.mjs +111 -0
  42. package/scripts/core/contracts.mjs +161 -0
  43. package/scripts/core/host-cli.mjs +204 -0
  44. package/scripts/core/host-model.mjs +323 -0
  45. package/scripts/core/host-probe.mjs +16 -0
  46. package/scripts/core/inactive-window.mjs +32 -0
  47. package/scripts/core/inactive-window.ps1 +36 -0
  48. package/scripts/core/kimi-host.mjs +41 -0
  49. package/scripts/core/kimi-runtime.mjs +140 -0
  50. package/scripts/core/lease-lock.ps1 +32 -0
  51. package/scripts/core/leases.mjs +176 -0
  52. package/scripts/core/maintenance-lock.mjs +77 -0
  53. package/scripts/core/native-argv.mjs +9 -0
  54. package/scripts/core/network-policy.mjs +18 -0
  55. package/scripts/core/openclaw-bootstrap.mjs +25 -0
  56. package/scripts/core/openclaw-config.mjs +35 -0
  57. package/scripts/core/openclaw-host.mjs +29 -0
  58. package/scripts/core/openclaw-runtime.mjs +33 -0
  59. package/scripts/core/openclaw-window.mjs +44 -0
  60. package/scripts/core/opencode-host.mjs +80 -0
  61. package/scripts/core/opencode-runtime.mjs +131 -0
  62. package/scripts/core/orchestrate-sdk.mjs +2595 -0
  63. package/scripts/core/pi-host.mjs +29 -0
  64. package/scripts/core/pi-runtime.mjs +54 -0
  65. package/scripts/core/pi-shutdown.mjs +16 -0
  66. package/scripts/core/poll-windows.mjs +48 -0
  67. package/scripts/core/print-profile.mjs +79 -0
  68. package/scripts/core/print-runtime.mjs +106 -0
  69. package/scripts/core/pty-host.mjs +38 -0
  70. package/scripts/core/recovery.mjs +75 -0
  71. package/scripts/core/run-board.mjs +155 -0
  72. package/scripts/core/run-guardian.mjs +130 -0
  73. package/scripts/core/runner.mjs +274 -0
  74. package/scripts/core/runtime-context.mjs +23 -0
  75. package/scripts/core/unit-carrier.mjs +55 -0
  76. package/scripts/core/unit-command.mjs +96 -0
  77. package/scripts/core/unit-runtime.mjs +107 -0
  78. package/scripts/gate.mjs +162 -0
  79. package/scripts/host-cli.mjs +2 -0
  80. package/scripts/install-deps.mjs +58 -0
  81. package/scripts/maintenance-lock.mjs +46 -0
  82. package/scripts/network-policy.mjs +2 -0
  83. package/scripts/open-tui-orchestrator-force.mjs +239 -0
  84. package/scripts/open-tui-orchestrator-preflight.mjs +85 -0
  85. package/scripts/orchestrate-sdk.mjs +59 -0
  86. package/scripts/package-lock.json +242 -0
  87. package/scripts/package.json +9 -0
  88. package/scripts/platform-guard.mjs +23 -0
  89. package/scripts/poll-windows.mjs +8 -0
  90. package/scripts/release-integrity.mjs +94 -0
  91. package/scripts/runtime-context.mjs +2 -0
  92. package/scripts/sdk-dependency-check.mjs +32 -0
  93. package/scripts/todo-list.mjs +89 -0
@@ -0,0 +1,161 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import crypto from 'node:crypto';
4
+ import {spawnSync} from 'node:child_process';
5
+
6
+ export const CORE_VERSION = '1.14.6';
7
+ export const SUCCESS = new Set(['verified']);
8
+ export const TERMINAL = new Set(['verified','failed','blocked','cancelled','awaiting-verification','preflight-failed','popup-failed','timeout','needs-reconciliation']);
9
+
10
+ // 内存 → 最大并发 TUI 窗数:先由内存 GiB 换算出厂商标称内存 GB(round(GiB × 1024 ÷ 1000)),再取其 3/4(round(标称GB × 3 ÷ 4))。
11
+ // 例如 16G 标称 → 12 台并发,4G 标称 → 3 台;不再有固定 8 硬上限。下限由调用方以 <4G 判定为不支持。为可测试而独立成函数。
12
+ // fresh 规划与 --resume-run 的恢复校验共用同一容量(contracts 放置以避免 recovery↔sdk 循环依赖)。
13
+ export function computeMaxWindows(memGb) {
14
+ const nominalGb = Math.round((memGb * 1024) / 1000);
15
+ return Math.round((nominalGb * 3) / 4);
16
+ }
17
+
18
+ export function summaryComplete(sum, strict = true) {
19
+ if (!sum?.runId || !Array.isArray(sum.blocks) || !Array.isArray(sum.tasks) || !sum.blocks.length || !sum.tasks.length) return false;
20
+ const exact = (ids, obj) => ids.length === new Set(ids).size && Object.keys(obj || {}).length === ids.length && ids.every(id => Object.hasOwn(obj || {}, id));
21
+ if (!exact(sum.blocks.map(b=>b.key),sum.results) || !exact(sum.tasks.map(t=>t.id),sum.taskResults)) return false;
22
+ const statuses = strict ? SUCCESS : TERMINAL;
23
+ return [...Object.values(sum.results),...Object.values(sum.taskResults)].every(r=>statuses.has(r?.status));
24
+ }
25
+
26
+ export function canonicalKey(key, cwd = process.cwd()) {
27
+ const match = /^(file|dir):(.+)$/i.exec(String(key));
28
+ if (!match) return String(key);
29
+ const windows = /^[a-z]:[\\/]|^\\\\/i.test(match[2]) || process.platform === 'win32';
30
+ const api = windows ? path.win32 : path.posix;
31
+ let p = api.resolve(cwd, match[2]);
32
+ try { p = fs.realpathSync.native(p); } catch {
33
+ // Resolve existing ancestors too, so a not-yet-created file beneath a junction
34
+ // conflicts with the same target reached through its real directory.
35
+ let parent=p; const tail=[];
36
+ while (api.dirname(parent)!==parent && !fs.existsSync(parent)) {tail.unshift(api.basename(parent));parent=api.dirname(parent);}
37
+ try {p=api.join(fs.realpathSync.native(parent),...tail);} catch {}
38
+ }
39
+ p=p.replaceAll('\\','/').replace(/\/+$/,'');
40
+ return match[1].toLowerCase()+':'+(windows?p.toLowerCase():p);
41
+ }
42
+ export function keysOverlap(a,b) {
43
+ return (a||[]).some(x=>(b||[]).some(y=>{
44
+ x=canonicalKey(x); y=canonicalKey(y);
45
+ if(x===y)return true;
46
+ const xm=/^(file|dir):(.*)$/.exec(x),ym=/^(file|dir):(.*)$/.exec(y);
47
+ if(!xm||!ym)return false;
48
+ return xm[2]===ym[2] || (xm[1]==='dir'&&ym[2].startsWith(xm[2]+'/')) || (ym[1]==='dir'&&xm[2].startsWith(ym[2]+'/'));
49
+ }));
50
+ }
51
+
52
+ export function levelWaves(recs) {
53
+ const aliases=new Map();
54
+ for(const r of recs) for(const alias of [r.key,r.title,r.b?.title].filter(Boolean)) {
55
+ if(aliases.has(alias)&&aliases.get(alias)!==r.key)throw new Error('Ambiguous block identity: '+alias);
56
+ aliases.set(alias,r.key);
57
+ }
58
+ for(const r of recs) r.dependsOn=[...new Set((r.dependsOn||[]).map(d=>{
59
+ if(!aliases.has(d))throw new Error('Unknown dependency: '+d);
60
+ const key=aliases.get(d); if(key===r.key)throw new Error('Self dependency: '+key);return key;
61
+ }))];
62
+ const pending=[...recs],done=new Set(),waves=[];
63
+ while(pending.length) {
64
+ const ready=pending.filter(r=>r.dependsOn.every(d=>done.has(d)));
65
+ if(!ready.length)throw new Error('Cyclic dependencies: '+pending.map(r=>r.key).join(', '));
66
+ waves.push(ready);for(const r of ready){done.add(r.key);pending.splice(pending.indexOf(r),1);}
67
+ }
68
+ return waves;
69
+ }
70
+
71
+ export function requestTasks(request) {
72
+ const text=String(request),parts=[];let start=0,quote='',depth=0;
73
+ for(let i=0;i<text.length;i++) {
74
+ const c=text[i];
75
+ if(quote){if(c===quote&&text[i-1]!=='\\'&&text[i-1]!=='`')quote='';continue;}
76
+ if(c==="'"&&/[\p{L}\p{N}]/u.test(text[i-1]||'')&&/[\p{L}\p{N}]/u.test(text[i+1]||''))continue;
77
+ if(['"',"'",'`'].includes(c)){quote=c;continue;}
78
+ if('([{'.includes(c)){depth++;continue;}if(')]}'.includes(c)){depth=Math.max(0,depth-1);continue;}
79
+ const explicitNext=/^\s*(?:(?:项目|工程|project|proj)\s*[-/]?\s*\d+|(?:修复|优化|修改|更新|检查|创建)\s*\d{2,3}(?!\d))/i.test(text.slice(i+1));
80
+ if(!depth&&(/[;;\n]/.test(c)||(/[,,。]/.test(c)&&explicitNext))){parts.push(text.slice(start,i));start=i+1;}
81
+ }
82
+ parts.push(text.slice(start));
83
+ return parts.map(s=>s.trim()).filter(Boolean).map((prompt,i)=>({id:`T${String(i+1).padStart(3,'0')}`,prompt}));
84
+ }
85
+ export function bindClassifiedTasks(request,classified) {
86
+ const tasks=requestTasks(request),byId=new Map(tasks.map(t=>[t.id,t]));
87
+ if(!Array.isArray(classified)||classified.length!==tasks.length||new Set(classified.map(t=>t.id)).size!==tasks.length||classified.some(t=>!byId.has(t.id)))throw new Error('Classified task inventory mismatch');
88
+ return classified.map(t=>({...t,summary:byId.get(t.id).prompt,prompt:byId.get(t.id).prompt}));
89
+ }
90
+ export function ledgerPlan(request, candidate, capacity, compact) {
91
+ const tasks=requestTasks(request);
92
+ if(!tasks.length)throw new Error('Empty task inventory');
93
+ let plan=structuredClone(candidate),assigned=new Set(),valid=true;
94
+ for(const block of plan) for(const task of block.tasks||[]) {
95
+ task.sourceIds=tasks.filter(t=>String(task.prompt||'').includes(t.prompt)).map(t=>t.id);
96
+ if(!task.sourceIds.length)valid=false;
97
+ for(const id of task.sourceIds){if(assigned.has(id))valid=false;assigned.add(id);}
98
+ }
99
+ // A model's omission or paraphrase must never discard the original instructions.
100
+ // Retain the entire inventory in one serial block when coverage is uncertain.
101
+ if(!valid||assigned.size!==tasks.length) plan=[{type:'linked',title:'complete-inventory',dependsOn:[],conflictKeys:[],tasks:tasks.map(t=>({...t,title:t.id,sourceIds:[t.id]}))}];
102
+ plan=compact(plan,capacity);
103
+ const ids=plan.flatMap(b=>b.tasks.flatMap(t=>t.sourceIds||[]));
104
+ if(ids.length!==tasks.length||new Set(ids).size!==tasks.length)throw new Error('Task inventory mismatch');
105
+ levelWaves(plan.map((b,i)=>({key:`B${i+1}`,title:b.title,b,dependsOn:b.dependsOn||[]})));
106
+ return {plan,tasks};
107
+ }
108
+
109
+ export function sha256(file) {return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');}
110
+
111
+ // 裸名可执行文件在 Windows 上会先按进程 cwd(编排器的 cwd = 任意工作区)解析,
112
+ // 在不受信目录里运行时构成二进制投毒入口。一律锚定到 SystemRoot;锚定目标不存在才回退裸名。
113
+ export function systemToolExe(name, env = process.env) {
114
+ const anchored = path.join(env.SystemRoot || env.WINDIR || 'C:\\Windows', 'System32', String(name));
115
+ return fs.existsSync(anchored) ? anchored : String(name);
116
+ }
117
+ export function powershellExe(env = process.env) {
118
+ const anchored = path.join(env.SystemRoot || env.WINDIR || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
119
+ return fs.existsSync(anchored) ? anchored : 'powershell.exe';
120
+ }
121
+ export function verifyTask(receipt, task, checks, context) {
122
+ if(!receipt || receipt.runId!==context.runId || receipt.token!==context.token || receipt.id!==task.id || receipt.status!=='completed')return {status:'failed',reason:'Missing or mismatched task receipt'};
123
+ return verifyChecks(checks,context.workspace);
124
+ }
125
+ // 校验 acceptance 契约 key 与 task id 的一致性:unknown key(拼错/多余)= 直接报错;task 无 check = 告警(将停在 awaiting-verification)。
126
+ export function acceptanceKeyDiff(checks,tasks) {
127
+ if(!checks||typeof checks!=='object'||Array.isArray(checks)||!Object.keys(checks).length)return null;
128
+ const ids=[...(new Set((tasks||[]).map(t=>t?.id).filter(Boolean)))],keys=Object.keys(checks);
129
+ const unknown=keys.filter(k=>!ids.includes(k)),missing=ids.filter(k=>!keys.includes(k));
130
+ return (unknown.length||missing.length)?{unknown,missing}:null;
131
+ }
132
+
133
+ export function verifyChecks(checks,workspace) {
134
+ if(!Array.isArray(checks)||!checks.length)return {status:'awaiting-verification',reason:'No parent-defined acceptance checks; window report is not proof of success'};
135
+ const evidence=[];
136
+ try {
137
+ for(const check of checks) {
138
+ if(check.type==='command') {
139
+ if(!check.command||!Array.isArray(check.args))throw new Error('Command acceptance requires command and args');
140
+ const result=spawnSync(check.command,check.args,{cwd:workspace,encoding:'utf8',windowsHide:true,shell:false,timeout:check.timeoutMs||30000});
141
+ if(result.error||result.status!==(check.exitCode??0))throw new Error('Acceptance command failed: '+(result.error?.message||result.status));
142
+ if(check.stdout!==undefined&&result.stdout.trim()!==check.stdout)throw new Error('Acceptance stdout mismatch');
143
+ evidence.push({type:'command',command:check.command,args:check.args,exitCode:result.status});continue;
144
+ }
145
+ const file=path.resolve(workspace,check.path);
146
+ if(check.type==='file') {
147
+ if(!fs.existsSync(file))throw new Error('Acceptance file missing: '+file+(String(check.path)!==file?' (check.path="'+check.path+'")':''));
148
+ if(!fs.statSync(file).isFile())throw new Error('Not a file: '+file);if(check.sha256&&sha256(file)!==check.sha256)throw new Error('Hash mismatch: '+file);}
149
+ else if(check.type==='json') {
150
+ if(!fs.existsSync(file))throw new Error('Acceptance file missing: '+file+(String(check.path)!==file?' (check.path="'+check.path+'")':''));
151
+ let value;try{value=JSON.parse(fs.readFileSync(file,'utf8'));}catch(e){throw new Error('Acceptance JSON parse failed: '+file+': '+e.message);}
152
+ for(const [key,expected] of Object.entries(check.equals||{})){
153
+ if(!(key in value))throw new Error('JSON field missing: '+file+' key="'+key+'" expected='+JSON.stringify(expected));
154
+ if(JSON.stringify(value[key])!==JSON.stringify(expected))throw new Error('JSON mismatch: '+file+' key="'+key+'" expected='+JSON.stringify(expected)+' actual='+JSON.stringify(value[key]));}}
155
+ else if(check.type==='absent') {if(fs.existsSync(file))throw new Error('Unexpected path: '+file);}
156
+ else throw new Error('Unsupported acceptance check: '+check.type);
157
+ evidence.push({type:check.type,path:file,sha256:check.type==='absent'?undefined:sha256(file)});
158
+ }
159
+ return {status:'verified',evidence};
160
+ } catch(e){return {status:'failed',reason:e.message};}
161
+ }
@@ -0,0 +1,204 @@
1
+ import fs from 'node:fs';
2
+ import { networkArgs } from './network-policy.mjs';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import {resolvePiHost,piArgs} from './pi-host.mjs';
6
+ import {resolveOpenClawHost} from './openclaw-host.mjs';
7
+ import {resolveOpenCodeHost,opencodeArgs} from './opencode-host.mjs';
8
+ import {resolveKimiHost,kimiArgs} from './kimi-host.mjs';
9
+ import {resolveClaudeHost,claudeArgs} from './claude-host.mjs';
10
+ import {resolveProfileHost,profileArgs,listProfiles,loadProfile} from './print-profile.mjs';
11
+ import {loadUnitProfile,expandUnitArgs} from './unit-command.mjs';
12
+
13
+ // Adapter contract: resolve a real CLI before any planner/window is started.
14
+ // Additional adapters must implement planning, interactive argv and completion
15
+ // capture together; a CLI name alone is not proof of compatibility.
16
+ export const adapters = Object.freeze({
17
+ openclaw: Object.freeze({id:'openclaw',label:'OpenClaw CLI',native:false}),
18
+ pi: Object.freeze({id:'pi',label:'pi agent',native:false,completionMarker:'__ORCH_DONE__',verified:false}),
19
+ opencode: Object.freeze({id:'opencode',label:'opencode',native:false,completionMarker:'__ORCH_DONE__',verified:false}),
20
+ kimi: Object.freeze({id:'kimi',label:'Kimi Code CLI',native:true,completionMarker:'__ORCH_DONE__',verified:false}),
21
+ claude: Object.freeze({id:'claude',label:'Claude Code CLI',native:true,completionMarker:'__ORCH_DONE__',verified:false}),
22
+ // carrier: TUI 类宿主需要真实终端——无头模式走 ConPTY 伪终端载体(真实控制台、完全不可见);
23
+ // 其余宿主为 print 类,无控制台的隐藏进程即可。载体解析与分级见 scripts/core/unit-carrier.mjs。
24
+ codex: Object.freeze({ id: 'codex', label: 'Codex CLI', native: true, carrier: 'pty',
25
+ modelEnv: 'ORCH_MODEL', effortEnv: 'ORCH_EFFORT',
26
+ completionMarker: '__ORCH_DONE__', verified: false }),
27
+ });
28
+
29
+ // Host markers on the Windows ancestor chain: the initiating agent is the
30
+ // NEAREST ancestor process matching a known CLI (a nested window that starts
31
+ // its own orchestration therefore binds to its own agent, not the outer one).
32
+ const HOST_CHAIN_MATCHERS = Object.freeze([
33
+ ['kimi', Object.freeze({ name: /^kimi\.exe$/i })],
34
+ ['claude', Object.freeze({ name: /^claude\.exe$/i, cmd: /[\\/]@anthropic-ai[\\/]claude-code[\\/]/i })],
35
+ ['codex', Object.freeze({ name: /^codex\.exe$/i, cmd: /[\\/]codex\.js([\s"']|$)|[\\/]@openai[\\/]codex[\\/]/i })],
36
+ ['pi', Object.freeze({ cmd: /[\\/]pi-coding-agent[\\/]/i })],
37
+ ['opencode', Object.freeze({ name: /^opencode\.exe$/i, cmd: /[\\/]opencode-ai[\\/]/i })],
38
+ ['openclaw', Object.freeze({ name: /^openclaw(\.exe)?$/i, cmd: /[\\/]openclaw[\\/]openclaw\.mjs/i })],
39
+ ]);
40
+
41
+ export function detectHostFromChain(chain) {
42
+ for (const proc of chain || []) {
43
+ const name = String(proc?.name || '');
44
+ const cmd = String(proc?.cmdline ?? proc?.commandLine ?? '');
45
+ for (const [id, m] of HOST_CHAIN_MATCHERS) {
46
+ if ((m.name && m.name.test(name)) || (m.cmd && m.cmd.test(cmd))) return id;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ // One CIM snapshot walked in memory (a single PowerShell call, ~1s). Only runs
53
+ // when no env marker identified the host; failure degrades to an empty chain.
54
+ export function hostProcessChain({ platform = process.platform, startPid = process.ppid, env = process.env, spawn = spawnSync } = {}) {
55
+ if (platform !== 'win32') return [];
56
+ const script = '$a=@{};Get-CimInstance Win32_Process|ForEach-Object{$a[[string]$_.ProcessId]=$_};$p=$a[[string]' + (Number(startPid) || 0) + '];$g=0;while($p -and $g -lt 16){$c=if($p.CommandLine){$p.CommandLine}else{""};("{0}`t{1}`t{2}" -f $p.ProcessId,$p.Name,$c);$p=$a[[string]$p.ParentProcessId];$g++}';
57
+ let r;
58
+ try {
59
+ const ps = path.join(env.SystemRoot || env.WINDIR || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
60
+ r = spawn(fs.existsSync(ps) ? ps : 'powershell.exe', ['-NoProfile', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], { encoding: 'utf8', windowsHide: true, shell: false, timeout: 8000 });
61
+ } catch { return []; }
62
+ if (r?.status !== 0) return [];
63
+ return String(r.stdout || '').split(/\r?\n/).filter(Boolean).map((line) => {
64
+ const [pid, name, ...rest] = line.split('\t');
65
+ return { pid: Number(pid), name, cmdline: rest.join('\t') };
66
+ });
67
+ }
68
+
69
+ export function detectHostAgent(env = process.env, { chain } = {}) {
70
+ const explicit = String(env.ORCH_AGENT || '').trim().toLowerCase();
71
+ if (explicit) return explicit;
72
+ if (env.CODEX_THREAD_ID || env.CODEX_SESSION_ID) return 'codex';
73
+ if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return 'claude';
74
+ // profile-class agents: env markers first, then ancestor process name/cmd patterns.
75
+ for (const pid of listProfiles(env)) {
76
+ const p = loadProfile(pid, env);
77
+ if (p && (p.envMarkers || []).some((m) => env[m])) return pid;
78
+ }
79
+ const procs = chain ?? hostProcessChain({ env });
80
+ const detected = detectHostFromChain(procs);
81
+ if (detected) return detected;
82
+ for (const pid of listProfiles(env)) {
83
+ const p = loadProfile(pid, env);
84
+ if (!p) continue;
85
+ let patterns = [];
86
+ try { patterns = (p.processNames || []).map((s) => new RegExp(s, 'i')); } catch { patterns = []; }
87
+ for (const proc of procs) {
88
+ const name = String(proc?.name || ''), cmd = String(proc?.cmdline || '');
89
+ if (patterns.some((re) => re.test(name) || re.test(cmd))) return pid;
90
+ }
91
+ }
92
+ throw new Error('HOST_UNKNOWN: set ORCH_AGENT to the initiating agent; no Codex fallback. Any agent can also bypass host identity entirely by supplying its own unit command (--unit-cmd/--unit-cmd-file, see docs/caller-driven.md).');
93
+ }
94
+
95
+ export function cliCandidates(env = process.env, platform = process.platform, {where = spawnSync} = {}) {
96
+ const explicit = env.ORCH_CLI || env.CODEX_EXE;
97
+ if (explicit) return [path.resolve(explicit)];
98
+ const found = [];
99
+ for (const dir of String(env.PATH || env.Path || '').split(path.delimiter).filter(Boolean)) {
100
+ found.push(path.join(dir, platform === 'win32' ? 'codex.exe' : 'codex'));
101
+ // npm shims are not native executables; the SDK must receive the packaged binary.
102
+ const modules = path.join(dir, 'node_modules', '@openai');
103
+ for (const base of [path.join(modules, 'codex', 'vendor'),
104
+ path.join(modules, 'codex', 'node_modules', '@openai', 'codex-win32-x64', 'vendor'),
105
+ path.join(modules, 'codex-win32-x64', 'vendor'),
106
+ path.join(modules, 'codex', 'node_modules', '@openai', 'codex-win32-arm64', 'vendor')]) {
107
+ for (const triple of ['x86_64-pc-windows-msvc', 'aarch64-pc-windows-msvc']) {
108
+ // npm 包把原生 exe 放在 vendor/<triple>/bin/;历史版本曾放在 vendor/<triple>/codex/。
109
+ for (const sub of ['bin', 'codex']) found.push(path.join(base, triple, sub, 'codex.exe'));
110
+ }
111
+ }
112
+ }
113
+ // Some desktop hosts expose neither PATH nor Path to a child process even
114
+ // though the native Codex installation is registered. Ask the Windows
115
+ // resolver for codex.exe in that case; this never installs or selects a shim.
116
+ if (platform === 'win32' && !found.some(file => { try { return fs.existsSync(file); } catch { return false; } })) {
117
+ try {
118
+ const whereExe = path.join(env.SystemRoot || env.WINDIR || 'C:\\Windows', 'System32', 'where.exe');
119
+ const result = where(whereExe, ['codex.exe'], {encoding:'utf8', windowsHide:true, shell:false, timeout:8000});
120
+ if (result?.status === 0) for (const line of String(result.stdout || '').split(/\r?\n/)) {
121
+ const candidate = line.trim();
122
+ if (candidate) found.push(candidate);
123
+ }
124
+ } catch { /* resolveHost reports CLI_MISSING if the native resolver is unavailable */ }
125
+ }
126
+ return [...new Set(found)];
127
+ }
128
+
129
+ export function resolveHost({ env = process.env, agent, candidates = cliCandidates,
130
+ probe = (file) => {
131
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return false;
132
+ const result = spawnSync(file, ['--version'], {
133
+ encoding: 'utf8', windowsHide: true, timeout: 8000, shell: false,
134
+ });
135
+ return result.status === 0 && /^codex-cli\s/m.test(result.stdout || '');
136
+ } } = {}) {
137
+ // 调用方自助通道(caller-driven):显式给出的单元画像最高优先——不依赖宿主识别(不查进程链/env 标记)、
138
+ // 不 probe、无名单限制。任何 agent(含没有适配器的)都能这样调用编排器。
139
+ const unit = loadUnitProfile(env);
140
+ if (unit) return { agent: String(env.ORCH_AGENT || unit.id || 'custom').trim().toLowerCase(), config: {
141
+ id: unit.id, label: unit.label, bin: unit.bin, prefixArgs: [], available: true, native: true,
142
+ completionMarker: '__ORCH_DONE__', modelEnv: 'ORCH_MODEL', effortEnv: 'ORCH_EFFORT', carrier: unit.carrier || null, unit, reusable: true,
143
+ } };
144
+ const id = agent || detectHostAgent(env);
145
+ if(id==='pi')return resolvePiHost(env);
146
+ if(id==='openclaw')return resolveOpenClawHost(env);
147
+ if(id==='opencode')return resolveOpenCodeHost(env);
148
+ if(id==='kimi')return resolveKimiHost(env);
149
+ if(id==='claude')return resolveClaudeHost(env);
150
+ // print-class 通用适配:有画像文件(scripts/core/agent-profiles/<id>.json)的 agent
151
+ // 走数据驱动的通用解析;画像存在即权威(CLI 缺失时 fail-closed,不回退)。
152
+ const profileHost = resolveProfileHost(env,id);
153
+ if (profileHost) return profileHost;
154
+ const adapter = adapters[id];
155
+ if (!adapter) throw new Error(`ADAPTER_UNSUPPORTED: ${id}; no CLI adapter implemented; no fallback or automatic install. Two supported paths: (1) supply your own unit command with --unit-cmd/--unit-cmd-file (any CLI, see docs/caller-driven.md); (2) node scripts/adapt.mjs --agent ${id} for print-class auto-adaptation.`);
156
+ const bin = candidates(env).find(probe);
157
+ if (!bin) throw new Error(`CLI_MISSING: ${id}; install its CLI yourself or set ORCH_CLI to its native executable. No fallback or automatic install.`);
158
+ return { agent: id, config: { ...adapter, bin, available: true } };
159
+ }
160
+
161
+ export function launchArgs(config, options = {}) {
162
+ // 调用方自助通道:argv 完全来自单元命令模板(占位符展开),不套用任何宿主专用参数。
163
+ if(config?.unit)return expandUnitArgs(config.unit,{prompt:'',promptFile:'',workspace:options.cwd||process.cwd()});
164
+ if(config?.id==='openclaw'){
165
+ if(options.sandbox&&options.sandbox!=='danger-full-access')throw new Error('OPENCLAW_SANDBOX_UNSUPPORTED');
166
+ return [];
167
+ }
168
+ if(config?.id==='pi'){
169
+ if(options.sandbox&&options.sandbox!=='danger-full-access')throw new Error('PI_SANDBOX_UNSUPPORTED: pi does not implement the requested sandbox');
170
+ return piArgs(config,{model:process.env.ORCH_MODEL,effort:process.env.ORCH_EFFORT});
171
+ }
172
+ if(config?.id==='opencode'){
173
+ if(options.sandbox&&options.sandbox!=='danger-full-access')throw new Error('OPENCODE_SANDBOX_UNSUPPORTED: opencode does not implement the requested sandbox');
174
+ return opencodeArgs(config,{model:process.env.ORCH_MODEL,effort:process.env.ORCH_EFFORT,cwd:options.cwd||process.cwd()});
175
+ }
176
+ if(config?.id==='kimi'){
177
+ if(options.sandbox&&options.sandbox!=='danger-full-access')throw new Error('KIMI_SANDBOX_UNSUPPORTED: kimi does not implement the requested sandbox');
178
+ return kimiArgs(config,{model:process.env.ORCH_MODEL});
179
+ }
180
+ if(config?.id==='claude'){
181
+ if(options.sandbox&&options.sandbox!=='danger-full-access')throw new Error('CLAUDE_SANDBOX_UNSUPPORTED: claude does not implement the requested sandbox');
182
+ return claudeArgs(config,{model:process.env.ORCH_MODEL,effort:process.env.ORCH_EFFORT});
183
+ }
184
+ if(config?.profile){
185
+ if(options.sandbox&&options.sandbox!=='danger-full-access')throw new Error('PROFILE_SANDBOX_UNSUPPORTED: '+config.id+' does not implement the requested sandbox');
186
+ return profileArgs(config,{model:process.env.ORCH_MODEL,effort:process.env.ORCH_EFFORT});
187
+ }
188
+ if (!adapters[config?.id]) throw new Error('ADAPTER_UNSUPPORTED: refusing Codex argv for another agent.');
189
+ const cwd = options.cwd || process.cwd();
190
+ const sandbox = options.sandbox || 'danger-full-access';
191
+ // Codex treats --dangerously-bypass-approvals-and-sandbox as a complete
192
+ // authorization envelope and rejects it when -a/--ask-for-approval is also
193
+ // supplied. Keep the explicit no-approval flag only for restricted modes.
194
+ const args = ['--cd', cwd];
195
+ if (sandbox === 'danger-full-access') args.push('--sandbox', sandbox);
196
+ else args.push('-a', 'never', '--sandbox', sandbox);
197
+ // The orchestrator has already received the initiating window's effective
198
+ // authorization and normalizes it to never + danger-full-access. Carry that
199
+ // envelope into Codex so project trust and command confirmations cannot stop
200
+ // an authorized TUI window at startup. Restricted callers keep their policy.
201
+ if (sandbox === 'danger-full-access') args.push('--dangerously-bypass-approvals-and-sandbox');
202
+ args.push('-c', `projects.${JSON.stringify(cwd)}.trust_level="trusted"`, ...networkArgs());
203
+ return args;
204
+ }