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.
- package/CHANGELOG.md +205 -0
- package/INSTALL-zh.md +96 -0
- package/INSTALL.md +96 -0
- package/LICENSE +48 -0
- package/README-zh.md +181 -0
- package/README.md +181 -0
- package/cli.mjs +37 -0
- package/docs/adapt.md +103 -0
- package/docs/assets/kimicode-agent-swarm-10-subagents.png +0 -0
- package/docs/auto-recovery.md +23 -0
- package/docs/caller-driven.md +121 -0
- package/docs/claude-adapter.md +25 -0
- package/docs/execution-contract.md +70 -0
- package/docs/inactive-windows.md +11 -0
- package/docs/kimi-adapter.md +27 -0
- package/docs/kimi-integration.md +56 -0
- package/docs/maintenance-lock.md +32 -0
- package/docs/openclaw-adapter.md +59 -0
- package/docs/openclaw-assessment-2026-09-06.md +59 -0
- package/docs/opencode-adapter.md +25 -0
- package/docs/pi-adapter.md +58 -0
- package/docs/public-readiness.md +63 -0
- package/docs/release-policy.md +39 -0
- package/docs/security-audit-2026-09-09.md +41 -0
- package/docs/trust-and-safety.md +64 -0
- package/docs/verification-2026-09-06.md +22 -0
- package/docs/verification-recovery-2026-09-06.md +36 -0
- package/orch.mjs +20 -0
- package/package.json +36 -0
- package/release.json +116 -0
- package/repair.mjs +228 -0
- package/scripts/adapt.mjs +35 -0
- package/scripts/agent-auth-prompt.txt +10 -0
- package/scripts/agent.mjs +1 -0
- package/scripts/core/adapt-lib.mjs +219 -0
- package/scripts/core/agent-auth-prompt.txt +10 -0
- package/scripts/core/agent-profiles/hermes.json +59 -0
- package/scripts/core/agent.mjs +1 -0
- package/scripts/core/checkpoint.mjs +38 -0
- package/scripts/core/claude-host.mjs +50 -0
- package/scripts/core/claude-runtime.mjs +111 -0
- package/scripts/core/contracts.mjs +161 -0
- package/scripts/core/host-cli.mjs +204 -0
- package/scripts/core/host-model.mjs +323 -0
- package/scripts/core/host-probe.mjs +16 -0
- package/scripts/core/inactive-window.mjs +32 -0
- package/scripts/core/inactive-window.ps1 +36 -0
- package/scripts/core/kimi-host.mjs +41 -0
- package/scripts/core/kimi-runtime.mjs +140 -0
- package/scripts/core/lease-lock.ps1 +32 -0
- package/scripts/core/leases.mjs +176 -0
- package/scripts/core/maintenance-lock.mjs +77 -0
- package/scripts/core/native-argv.mjs +9 -0
- package/scripts/core/network-policy.mjs +18 -0
- package/scripts/core/openclaw-bootstrap.mjs +25 -0
- package/scripts/core/openclaw-config.mjs +35 -0
- package/scripts/core/openclaw-host.mjs +29 -0
- package/scripts/core/openclaw-runtime.mjs +33 -0
- package/scripts/core/openclaw-window.mjs +44 -0
- package/scripts/core/opencode-host.mjs +80 -0
- package/scripts/core/opencode-runtime.mjs +131 -0
- package/scripts/core/orchestrate-sdk.mjs +2595 -0
- package/scripts/core/pi-host.mjs +29 -0
- package/scripts/core/pi-runtime.mjs +54 -0
- package/scripts/core/pi-shutdown.mjs +16 -0
- package/scripts/core/poll-windows.mjs +48 -0
- package/scripts/core/print-profile.mjs +79 -0
- package/scripts/core/print-runtime.mjs +106 -0
- package/scripts/core/pty-host.mjs +38 -0
- package/scripts/core/recovery.mjs +75 -0
- package/scripts/core/run-board.mjs +155 -0
- package/scripts/core/run-guardian.mjs +130 -0
- package/scripts/core/runner.mjs +274 -0
- package/scripts/core/runtime-context.mjs +23 -0
- package/scripts/core/unit-carrier.mjs +55 -0
- package/scripts/core/unit-command.mjs +96 -0
- package/scripts/core/unit-runtime.mjs +107 -0
- package/scripts/gate.mjs +162 -0
- package/scripts/host-cli.mjs +2 -0
- package/scripts/install-deps.mjs +58 -0
- package/scripts/maintenance-lock.mjs +46 -0
- package/scripts/network-policy.mjs +2 -0
- package/scripts/open-tui-orchestrator-force.mjs +239 -0
- package/scripts/open-tui-orchestrator-preflight.mjs +85 -0
- package/scripts/orchestrate-sdk.mjs +59 -0
- package/scripts/package-lock.json +242 -0
- package/scripts/package.json +9 -0
- package/scripts/platform-guard.mjs +23 -0
- package/scripts/poll-windows.mjs +8 -0
- package/scripts/release-integrity.mjs +94 -0
- package/scripts/runtime-context.mjs +2 -0
- package/scripts/sdk-dependency-check.mjs +32 -0
- package/scripts/todo-list.mjs +89 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {spawnSync} from 'node:child_process';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import {probeCli} from './host-probe.mjs';
|
|
6
|
+
import {profilesDir,loadProfile} from './print-profile.mjs';
|
|
7
|
+
import {detectHostAgent} from './host-cli.mjs';
|
|
8
|
+
import {readProfileKey,readSectionKey} from './host-model.mjs';
|
|
9
|
+
import {readMaintenanceLock,acquireMaintenanceLock,releaseMaintenanceLock} from './maintenance-lock.mjs';
|
|
10
|
+
|
|
11
|
+
// 自动适配(print-class 画像管线):
|
|
12
|
+
// 探测 CLI 表面 → 生成画像 → 解析自检 → 活体验证(真实调用一次,取回探活令牌)
|
|
13
|
+
// → 重封 → 自动提交(默认开)。任何一步失败都回滚,仓库保持原状(fail-closed)。
|
|
14
|
+
// 成功 = 表面探测 + 解析自检 + 活体验证;测试套件可用 --suite 追加。
|
|
15
|
+
|
|
16
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
17
|
+
const BUILTIN = new Set(['pi', 'openclaw', 'opencode', 'kimi', 'claude', 'codex']);
|
|
18
|
+
export const PROMPT_FLAG_CANDIDATES = ['-p', '--print', '-z', '--oneshot', '--prompt', '--message', '--ask', '--run', '--exec', '-m'];
|
|
19
|
+
export const APPROVE_CANDIDATES = ['--yes-always', '--dangerously-skip-permissions', '--yolo', '--auto', '--approve', '--full-auto', '--dangerously-bypass-approvals-and-sandbox'];
|
|
20
|
+
|
|
21
|
+
function run(cmd, args, opts = {}) {
|
|
22
|
+
return spawnSync(cmd, args, { encoding: 'utf8', windowsHide: true, shell: false, timeout: 120000, ...opts });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 精确候选发现:只读固定键名与固定候选路径,绝不猜测值。
|
|
26
|
+
// 供 adapt 写入画像的 modelDetection(engine 端 host-model.detectProfile 消费)。
|
|
27
|
+
export function discoverModelDetection(id, { home = process.env.USERPROFILE || process.env.HOME || '', env = process.env } = {}) {
|
|
28
|
+
const spec = {};
|
|
29
|
+
const up = String(id).toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
30
|
+
const envHit = [up + '_MODEL', up + '_DEFAULT_MODEL'].find((n) => env[n] && String(env[n]).trim());
|
|
31
|
+
if (envHit) spec.env = [envHit];
|
|
32
|
+
const effortEnvHit = [up + '_EFFORT', up + '_THINKING'].find((n) => env[n] && String(env[n]).trim());
|
|
33
|
+
if (effortEnvHit) spec.effortEnv = [effortEnvHit];
|
|
34
|
+
const MODEL_KEYS = ['model', 'defaultModel', 'default_model'];
|
|
35
|
+
const EFFORT_KEYS = ['effort', 'defaultEffort', 'thinking', 'defaultThinkingLevel'];
|
|
36
|
+
const candidates = [
|
|
37
|
+
'.' + id + '.conf.yml', '.' + id + '.conf.yaml', '.' + id + '.json',
|
|
38
|
+
path.join('.' + id, 'settings.json'), path.join('.' + id, 'config.json'),
|
|
39
|
+
path.join('.config', id, 'config.json'), path.join('.config', id, 'config.jsonc'),
|
|
40
|
+
path.join('.config', id, 'settings.json'),
|
|
41
|
+
];
|
|
42
|
+
if (home) for (const rel of candidates) {
|
|
43
|
+
const file = path.join(home, rel);
|
|
44
|
+
try { if (!fs.statSync(file).isFile()) continue; } catch { continue; }
|
|
45
|
+
let modelKey = '', effortKey = '';
|
|
46
|
+
for (const k of MODEL_KEYS) { try { if (readProfileKey(file, k)) { modelKey = k; break; } } catch { /* try next key */ } }
|
|
47
|
+
for (const k of EFFORT_KEYS) { try { if (readProfileKey(file, k)) { effortKey = k; break; } } catch { /* try next key */ } }
|
|
48
|
+
if (modelKey || effortKey) { spec.config = [{ file: '~/' + rel.split(path.sep).join('/'), modelKey: modelKey || undefined, effortKey: effortKey || undefined }]; break; }
|
|
49
|
+
}
|
|
50
|
+
// <ID>_HOME / <ID>_CONFIG_DIR(如 HERMES_HOME)指向的配置目录:v0 风格 config.yaml/json 里
|
|
51
|
+
// 档位常嵌在 model 节内(model.default / model.reasoning_effort)。落成 $VAR 引用,画像机器无关。
|
|
52
|
+
if (!spec.config) {
|
|
53
|
+
const homeVar = [up + '_HOME', up + '_CONFIG_DIR'].find((n) => env[n] && String(env[n]).trim());
|
|
54
|
+
if (homeVar) {
|
|
55
|
+
const homeDir = String(env[homeVar]).trim();
|
|
56
|
+
for (const name of ['config.yaml', 'config.yml', 'config.json']) {
|
|
57
|
+
const file = path.join(homeDir, name);
|
|
58
|
+
try { if (!fs.statSync(file).isFile()) continue; } catch { continue; }
|
|
59
|
+
let modelKey = '', effortKey = '', effortSection = '';
|
|
60
|
+
for (const k of ['default', 'name', ...MODEL_KEYS]) { try { if (readSectionKey(file, 'model', k)) { modelKey = k; break; } } catch { /* next */ } }
|
|
61
|
+
// effort 不一定与 model 同节(hermes:model.default 在 model 节、reasoning_effort 在 agent 节)
|
|
62
|
+
for (const sec of ['model', 'agent']) {
|
|
63
|
+
if (effortKey) break;
|
|
64
|
+
for (const k of ['reasoning_effort', ...EFFORT_KEYS]) { try { if (readSectionKey(file, sec, k)) { effortKey = k; effortSection = sec; break; } } catch { /* next */ } }
|
|
65
|
+
}
|
|
66
|
+
if (modelKey || effortKey) { spec.config = [{ file: '$' + homeVar + '/' + name, section: 'model', modelKey: modelKey || undefined, effortKey: effortKey || undefined, effortSection: effortSection && effortSection !== 'model' ? effortSection : undefined }]; break; }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return (spec.env || spec.effortEnv || spec.config) ? spec : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function locateCli(id, env = process.env, explicit) {
|
|
74
|
+
const path0 = explicit || env.ORCH_CLI || env[String(id).toUpperCase() + '_EXE'];
|
|
75
|
+
if (path0) {
|
|
76
|
+
const file = path.resolve(path0);
|
|
77
|
+
if (!fs.existsSync(file)) return { missing: file };
|
|
78
|
+
if (/\.m?js$/i.test(file)) { const sibling = path.join(path.dirname(file), 'node.exe'); return { bin: fs.existsSync(sibling) ? sibling : process.execPath, prefixArgs: [file], source: file }; }
|
|
79
|
+
return { bin: file };
|
|
80
|
+
}
|
|
81
|
+
const dirs = String(env.PATH || env.Path || '').split(path.delimiter).filter(Boolean);
|
|
82
|
+
const exe = dirs.map((d) => path.join(d, id + '.exe')).find((f) => { try { return fs.statSync(f).isFile(); } catch { return false; } });
|
|
83
|
+
if (exe) return { bin: exe };
|
|
84
|
+
const shim = [...dirs.map((d) => path.join(d, id + '.cmd')), ...dirs.map((d) => path.join(d, id))].find((f) => { try { return fs.statSync(f).isFile(); } catch { return false; } });
|
|
85
|
+
if (shim) return { shim };
|
|
86
|
+
return { missing: id };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function adaptAgent({ id, cli, env = process.env, live = true, commit = true, suite = false, force = false, writeProfile = true, smoke = false } = {}) {
|
|
90
|
+
const report = { id, cli: null, version: null, profile: null, live: null, smoke: null, committed: false, passed: false };
|
|
91
|
+
if (!id) { report.reason = 'no agent id (pass --agent or set ORCH_AGENT)'; return report; }
|
|
92
|
+
if (BUILTIN.has(id)) { report.reason = id + ' already has a dedicated adapter; nothing to adapt'; report.passed = true; report.skipped = 'builtin'; return report; }
|
|
93
|
+
const lock = readMaintenanceLock(env);
|
|
94
|
+
if (lock.locked && !force) { report.reason = 'maintenance lock is held; retry after unlock (or force)'; return report; }
|
|
95
|
+
const located = locateCli(id, env, cli);
|
|
96
|
+
if (located.shim) { report.reason = 'only a shell shim found at ' + located.shim + '; point --cli/ORCH_CLI at the real executable (.exe)'; return report; }
|
|
97
|
+
if (located.missing) { report.reason = 'CLI not found for ' + id + ' (looked on PATH; pass --cli/ORCH_CLI)'; return report; }
|
|
98
|
+
report.cli = located.source || located.bin;
|
|
99
|
+
const prefixArgs = located.prefixArgs || [];
|
|
100
|
+
|
|
101
|
+
const versionResult = probeCli(located.bin, [...prefixArgs, '--version'], { env });
|
|
102
|
+
const version = /(\d+\.\d+\.\d+)/.exec(String(versionResult.stdout || ''))?.[1];
|
|
103
|
+
if (versionResult.status !== 0 || !version) { report.reason = '--version probe failed: ' + (versionResult.error || 'exit ' + versionResult.status + ' out=' + JSON.stringify(String(versionResult.stdout || '').slice(0, 120))); return report; }
|
|
104
|
+
report.version = version;
|
|
105
|
+
|
|
106
|
+
const help = probeCli(located.bin, [...prefixArgs, '--help'], { env });
|
|
107
|
+
if (help.status !== 0) { report.reason = '--help probe failed: ' + (help.error || 'exit ' + help.status); return report; }
|
|
108
|
+
const helpText = help.out;
|
|
109
|
+
const flagBoundary = (f) => new RegExp('(^|[\\s,|\\[\\]()])' + f.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + '([\\s,|\\[\\]()]|$)', 'm');
|
|
110
|
+
// 模型旗标的短别名(形如 "-m MODEL, --model MODEL"):短别名绝不能被当作 prompt 旗标
|
|
111
|
+
// 误取——否则提示词会被喂给 model 参数、CLI 掉进交互模式(无终端即崩),探活与单元双失败。
|
|
112
|
+
const modelAlias = /(^|\n)\s*(-[A-Za-z])\s+[A-Z_.]+(?:\s*,\s*|\s{2,})--model\b/.exec(helpText)?.[2] || null;
|
|
113
|
+
const promptFlag = PROMPT_FLAG_CANDIDATES.find((f) => f !== modelAlias && flagBoundary(f).test(helpText));
|
|
114
|
+
if (!promptFlag) { report.reason = 'no one-shot prompt flag found (candidates: ' + PROMPT_FLAG_CANDIDATES.join(' ') + '); not a print-class CLI — needs a manual adapter'; return report; }
|
|
115
|
+
const approve = APPROVE_CANDIDATES.filter((f) => flagBoundary(f).test(helpText));
|
|
116
|
+
const model = flagBoundary('--model').test(helpText) ? ['--model'] : null;
|
|
117
|
+
const effortFlag = ['--effort', '--reasoning', '--thinking'].find((f) => flagBoundary(f).test(helpText));
|
|
118
|
+
const effort = effortFlag ? [effortFlag] : null;
|
|
119
|
+
const probeToken = 'ADAPTOK-' + Math.random().toString(36).slice(2, 10);
|
|
120
|
+
const profile = {
|
|
121
|
+
schema: 1,
|
|
122
|
+
id,
|
|
123
|
+
label: id,
|
|
124
|
+
bin: [id + '.exe', id + '.cmd', id],
|
|
125
|
+
helpFlags: [promptFlag, ...approve],
|
|
126
|
+
versionPattern: '(\\d+\\.\\d+\\.\\d+)',
|
|
127
|
+
probePrompt: 'reply with exactly ' + probeToken + ' and nothing else',
|
|
128
|
+
envMarkers: [],
|
|
129
|
+
processNames: ['^' + id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(\\.exe)?$'],
|
|
130
|
+
args: { approve, extra: [], model, effort, prompt: [promptFlag] },
|
|
131
|
+
adaptedAt: new Date().toISOString(),
|
|
132
|
+
adaptedFrom: { cli: path.basename(located.source || located.bin), version, prefixArgs: prefixArgs.length ? prefixArgs : undefined },
|
|
133
|
+
};
|
|
134
|
+
const detection = discoverModelDetection(id, { env });
|
|
135
|
+
if (detection) profile.modelDetection = detection;
|
|
136
|
+
report.modelDetection = detection || null;
|
|
137
|
+
report.profile = profile;
|
|
138
|
+
|
|
139
|
+
const profileFile = path.join(profilesDir(env), id + '.json');
|
|
140
|
+
fs.mkdirSync(path.dirname(profileFile), { recursive: true });
|
|
141
|
+
const prev = fs.existsSync(profileFile) ? fs.readFileSync(profileFile, 'utf8') : null;
|
|
142
|
+
const rollback = () => {
|
|
143
|
+
try { if (prev === null) fs.rmSync(profileFile, { force: true }); else fs.writeFileSync(profileFile, prev, 'utf8'); } catch { /* ignore */ }
|
|
144
|
+
};
|
|
145
|
+
if (writeProfile) fs.writeFileSync(profileFile, JSON.stringify(profile, null, 2) + '\n', 'utf8');
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const { resolveProfileHost } = await import('./print-profile.mjs');
|
|
149
|
+
const host = resolveProfileHost({ ...env, ORCH_AGENT_PROFILES_DIR: profilesDir(env), ORCH_CLI: located.source || located.bin }, id);
|
|
150
|
+
if (!host || host.config.version !== version) { report.reason = 'resolution self-check failed'; rollback(); return report; }
|
|
151
|
+
} catch (error) { report.reason = 'resolution self-check failed: ' + error.message; rollback(); return report; }
|
|
152
|
+
|
|
153
|
+
if (live) {
|
|
154
|
+
const { profileArgs } = await import('./print-profile.mjs');
|
|
155
|
+
const args = [...prefixArgs, ...profileArgs({ prefixArgs: [], profile }, {}), profile.probePrompt];
|
|
156
|
+
const t0 = Date.now();
|
|
157
|
+
const res = run(located.bin, args, { env, cwd: process.cwd(), timeout: 240000 });
|
|
158
|
+
const out = String(res.stdout || '') + String(res.stderr || '');
|
|
159
|
+
const answered = out.split(/\r?\n/).some((line) => line.includes(probeToken) && !/reply with exactly/i.test(line));
|
|
160
|
+
report.live = { exit: res.status, ms: Date.now() - t0, answered, tail: out.trim().slice(-400) };
|
|
161
|
+
if (!answered) { report.reason = 'live probe did not return the token (exit=' + res.status + ')'; rollback(); return report; }
|
|
162
|
+
} else report.live = { skipped: 'no-live' };
|
|
163
|
+
|
|
164
|
+
if (smoke) {
|
|
165
|
+
// 块级真实验收:用画像 agent 走一遍真实的 orch 单块执行(checkpoint 协议 + 验收契约),
|
|
166
|
+
// 把"能接上"验证到"跑得通"。消耗该 agent 的真实额度,需其已登录。
|
|
167
|
+
const os = await import('node:os');
|
|
168
|
+
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'orch-adapt-smoke-'));
|
|
169
|
+
try {
|
|
170
|
+
fs.writeFileSync(path.join(ws, 'acceptance.json'), JSON.stringify({ T001: [{ type: 'json', path: 'receipt.json', equals: { ok: true } }] }));
|
|
171
|
+
const smokeEnv = {
|
|
172
|
+
...env, ORCH_AGENT: id, ORCH_CLI: located.source || located.bin, ORCH_AGENT_PROFILES_DIR: profilesDir(env),
|
|
173
|
+
ORCH_ACCEPTANCE_FILE: path.join(ws, 'acceptance.json'), ORCH_MAINTENANCE_LOCK: path.join(ws, 'no-lock.json'),
|
|
174
|
+
ORCH_MAX_POLL_MS: '300000', WORKSPACE_DIR: ws, ORCH_STATE_DIR: path.join(ws, 'temp', 'orchestrator'),
|
|
175
|
+
};
|
|
176
|
+
delete smokeEnv.NODE_TEST_CONTEXT;
|
|
177
|
+
const t0 = Date.now();
|
|
178
|
+
const res = run(process.execPath, [path.join(REPO, 'orch.mjs'), '--run-id', 'adapt-smoke', '创建 receipt.json 内容为 {"ok":true} 并读回核验'], { env: smokeEnv, cwd: ws, timeout: 420000 });
|
|
179
|
+
let success = false;
|
|
180
|
+
try { success = JSON.parse(fs.readFileSync(path.join(ws, 'temp', 'orchestrator', 'runs', 'adapt-smoke', 'summary.json'), 'utf8')).success === true; } catch { /* no summary */ }
|
|
181
|
+
report.smoke = { exit: res.status, ms: Date.now() - t0, success };
|
|
182
|
+
if (!success) { report.reason = 'block-level smoke failed (exit=' + res.status + '); the CLI may need login/quota — rerun after configuring it'; rollback(); return report; }
|
|
183
|
+
} finally {
|
|
184
|
+
for (let i = 0; i < 5; i++) { try { fs.rmSync(ws, { recursive: true, force: true }); break; } catch { spawnSync('cmd', ['/c', 'rmdir', '/s', '/q', ws], { windowsHide: true }); } }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (suite) {
|
|
189
|
+
const s = run(process.execPath, ['--test', 'test/print-profile.test.mjs'], { cwd: REPO, env: { ...env, ORCH_AGENT: 'codex', ORCH_MAINTENANCE_LOCK: path.join(REPO, 'temp', 'adapt-suite-lock-none.json') }, timeout: 300000 });
|
|
190
|
+
report.suite = { exit: s.status };
|
|
191
|
+
if (s.status !== 0) { report.reason = 'print-profile suite failed'; rollback(); return report; }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (writeProfile && commit) {
|
|
195
|
+
acquireMaintenanceLock({ reason: 'auto-adapt ' + id, ttlMinutes: 30, env });
|
|
196
|
+
try {
|
|
197
|
+
const seal = run(process.execPath, [path.join(REPO, 'scripts', 'release-integrity.mjs'), '--seal'], { cwd: REPO, env });
|
|
198
|
+
if (seal.status !== 0) { report.reason = 'reseal failed: ' + String(seal.stderr || '').slice(0, 200); rollback(); run(process.execPath, [path.join(REPO, 'scripts', 'release-integrity.mjs'), '--seal'], { cwd: REPO, env }); return report; }
|
|
199
|
+
const rel = path.relative(REPO, profileFile).split(path.sep).join('/');
|
|
200
|
+
const add = run('git', ['add', '--', rel, 'release.json'], { cwd: REPO, env });
|
|
201
|
+
const commitResult = run('git', ['commit', '-m', 'feat(adapt): print-class profile for ' + id + ' (' + version + ') [auto-generated]'], { cwd: REPO, env });
|
|
202
|
+
report.committed = commitResult.status === 0;
|
|
203
|
+
report.gitAdd = add.status;
|
|
204
|
+
} finally { releaseMaintenanceLock(env); }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
report.passed = true;
|
|
208
|
+
return report;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function autoAdaptIfNeeded(env = process.env) {
|
|
212
|
+
if (readMaintenanceLock(env).locked) return { skipped: 'maintenance-lock' };
|
|
213
|
+
let id;
|
|
214
|
+
try { id = detectHostAgent(env); } catch { return { skipped: 'host-unknown' }; }
|
|
215
|
+
if (BUILTIN.has(id) || loadProfile(id, env)) return { skipped: 'already-adapted', id };
|
|
216
|
+
const report = await adaptAgent({ id, env, live: true, commit: true, writeProfile: true });
|
|
217
|
+
if (!report.passed) throw new Error('AUTO_ADAPT_FAILED: ' + id + ' — ' + (report.reason || 'unknown') + (report.live ? ' | live: exit=' + (report.live.exit ?? 'n/a') : ''));
|
|
218
|
+
return report;
|
|
219
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
本窗任务已经由用户授权执行;启动即开始完成指定任务,不再次询问 yes, continue 或请求重复确认。权限以本次 CLI 参数和宿主实际执行规则为准。
|
|
2
|
+
本窗执行主控分配的任务;授权仅来自用户请求及宿主规则,不因编排而扩大。
|
|
3
|
+
先检查相关文件与环境,再在上下文列出 TODO LIST(目标、依赖、动作、验证和收尾),随后完成任务。清单不必写成独立文件。
|
|
4
|
+
本窗不使用子代理;不得再调用 --run-windows、--spawn 或另开执行窗。联动与并行由主控统一安排。
|
|
5
|
+
遵守工作区 AGENTS.md、宿主沙箱及审批规则;不得绕过审批或改写信任配置。超出授权、缺少凭据或需要关键选择时如实报告阻塞,不伪报完成。
|
|
6
|
+
只修改本块范围内的文件,保留其它任务和用户的改动。适用且已安装的技能按其规则使用;不存在的技能不能当成必备工具。
|
|
7
|
+
修改后逐一真实读回产物,运行相应测试或检查;涉及中文时使用 UTF-8 并检查编码。只读任务以实际检查结果为产物,不强制创建文件。
|
|
8
|
+
最后汇报结果与验证证据;只有本块全部完成且验证通过,才在最后单独输出一行结束标记:
|
|
9
|
+
__ORCH_DONE__ <本窗token>
|
|
10
|
+
失败、阻塞或产物未落盘时禁止输出结束标记。主控仍须核对最终产物。
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"id": "hermes",
|
|
4
|
+
"label": "Hermes Agent CLI",
|
|
5
|
+
"bin": [
|
|
6
|
+
"hermes.exe",
|
|
7
|
+
"hermes.cmd",
|
|
8
|
+
"hermes"
|
|
9
|
+
],
|
|
10
|
+
"helpFlags": [
|
|
11
|
+
"-z",
|
|
12
|
+
"--oneshot",
|
|
13
|
+
"--reasoning",
|
|
14
|
+
"--yolo",
|
|
15
|
+
"--no-restore-cwd"
|
|
16
|
+
],
|
|
17
|
+
"versionPattern": "(\\d+\\.\\d+\\.\\d+)",
|
|
18
|
+
"probePrompt": "reply with exactly ADAPTOK-nvdry4b7 and nothing else",
|
|
19
|
+
"envMarkers": [
|
|
20
|
+
"HERMES_AGENT",
|
|
21
|
+
"HERMES_SESSION_ID"
|
|
22
|
+
],
|
|
23
|
+
"processNames": [
|
|
24
|
+
"^hermes(\\.exe)?$"
|
|
25
|
+
],
|
|
26
|
+
"args": {
|
|
27
|
+
"approve": [
|
|
28
|
+
"--yolo"
|
|
29
|
+
],
|
|
30
|
+
"extra": [
|
|
31
|
+
"--no-restore-cwd"
|
|
32
|
+
],
|
|
33
|
+
"model": [
|
|
34
|
+
"--model"
|
|
35
|
+
],
|
|
36
|
+
"effort": [
|
|
37
|
+
"--reasoning"
|
|
38
|
+
],
|
|
39
|
+
"prompt": [
|
|
40
|
+
"-z"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"adaptedAt": "2026-09-14T04:33:36.746Z",
|
|
44
|
+
"adaptedFrom": {
|
|
45
|
+
"cli": "hermes.exe",
|
|
46
|
+
"version": "0.21.2"
|
|
47
|
+
},
|
|
48
|
+
"modelDetection": {
|
|
49
|
+
"config": [
|
|
50
|
+
{
|
|
51
|
+
"file": "$HERMES_HOME/config.yaml",
|
|
52
|
+
"section": "model",
|
|
53
|
+
"modelKey": "default",
|
|
54
|
+
"effortKey": "reasoning_effort",
|
|
55
|
+
"effortSection": "agent"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { adapters as AGENTS, resolveHost as resolveAgent, launchArgs, detectHostAgent } from './host-cli.mjs';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import {atomicJson} from './leases.mjs';
|
|
5
|
+
|
|
6
|
+
export function recordCheckpoint(attemptFile,id,state) {
|
|
7
|
+
const attempt=JSON.parse(fs.readFileSync(attemptFile,'utf8'));
|
|
8
|
+
if(!attempt.taskIds.includes(id)||!['started','completed'].includes(state))throw new Error('Invalid task checkpoint');
|
|
9
|
+
const file=path.join(path.dirname(attemptFile),'checkpoints',id+'.json');
|
|
10
|
+
let previous;try{previous=JSON.parse(fs.readFileSync(file,'utf8'));}catch(e){if(e.code!=='ENOENT')throw e;}
|
|
11
|
+
if(previous!==undefined&&(!previous||previous.id!==id||previous.runId!==attempt.runId||previous.token!==attempt.token||!['started','completed'].includes(previous.state)))throw new Error('Invalid checkpoint identity or state');
|
|
12
|
+
if(previous?.state==='completed') {
|
|
13
|
+
if(state==='started')throw new Error('Task already completed; do not execute it again');
|
|
14
|
+
return previous;
|
|
15
|
+
}
|
|
16
|
+
if(state==='completed'&&previous?.state!=='started')throw new Error('Record started before executing a task');
|
|
17
|
+
if(state==='started'&&previous)throw new Error('Task already started; reconcile before retrying');
|
|
18
|
+
const record={id,runId:attempt.runId,token:attempt.token,state,startedAt:previous?.startedAt||Date.now(),updatedAt:Date.now()};
|
|
19
|
+
if(state==='started') {
|
|
20
|
+
fs.mkdirSync(path.dirname(file),{recursive:true});
|
|
21
|
+
const fd=fs.openSync(file,'wx');
|
|
22
|
+
try{fs.writeFileSync(fd,JSON.stringify(record));fs.fsyncSync(fd);}finally{fs.closeSync(fd);}
|
|
23
|
+
}else atomicJson(file,record);
|
|
24
|
+
return record;
|
|
25
|
+
}
|
|
26
|
+
export function readCheckpoints(attemptFile,expected) {
|
|
27
|
+
const attempt=JSON.parse(fs.readFileSync(attemptFile,'utf8')),out={};
|
|
28
|
+
if(expected&&(!attempt||attempt.runId!==expected.runId||attempt.token!==expected.token||JSON.stringify(attempt.taskIds)!==JSON.stringify(expected.taskIds)))throw new Error('Attempt identity mismatch');
|
|
29
|
+
for(const id of attempt.taskIds) {
|
|
30
|
+
const file=path.join(path.dirname(attemptFile),'checkpoints',id+'.json');
|
|
31
|
+
try{const r=JSON.parse(fs.readFileSync(file,'utf8'));out[id]=r.id===id&&r.runId===attempt.runId&&r.token===attempt.token&&['started','completed'].includes(r.state)?r:{state:'invalid'};}
|
|
32
|
+
catch(e){if(e.code!=='ENOENT')out[id]={state:'invalid'};}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
if(process.argv[1]&&path.resolve(process.argv[1]).toLowerCase()===fileURLToPath(import.meta.url).toLowerCase()) {
|
|
37
|
+
try{console.log(JSON.stringify(recordCheckpoint(...process.argv.slice(2))));}catch(e){console.error(e.message);process.exitCode=1;}
|
|
38
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {probeCli} from './host-probe.mjs';
|
|
4
|
+
const HELP_FLAGS=['--print','--dangerously-skip-permissions','--model'];
|
|
5
|
+
export function resolveClaudeHost(env=process.env) {
|
|
6
|
+
const explicit=env.ORCH_CLI||env.CLAUDE_EXE;
|
|
7
|
+
const candidates=explicit?[path.resolve(explicit)]:[
|
|
8
|
+
...String(env.PATH||env.Path||'').split(path.delimiter).filter(Boolean).flatMap(dir=>['claude.exe','claude.cmd','claude'].map(n=>path.join(dir,n))),
|
|
9
|
+
];
|
|
10
|
+
const rejected=[];
|
|
11
|
+
for(const file of [...new Set(candidates)]) {
|
|
12
|
+
if(!fs.existsSync(file)||!fs.statSync(file).isFile())continue;
|
|
13
|
+
let bin=file,prefixArgs=[];
|
|
14
|
+
if(!/\.exe$/i.test(file)) {
|
|
15
|
+
if(/\.[cm]?js$/i.test(file)) {
|
|
16
|
+
const sibling=path.join(path.dirname(file),'node.exe');bin=fs.existsSync(sibling)?sibling:process.execPath;prefixArgs=[file];
|
|
17
|
+
} else {
|
|
18
|
+
// npm/shell shim (`claude`, `claude.cmd`) -> resolve the packaged claude.exe.
|
|
19
|
+
let entry=null;
|
|
20
|
+
try {
|
|
21
|
+
const shim=fs.readFileSync(file,'utf8');
|
|
22
|
+
const match=shim.match(/node_modules[\\/]@anthropic-ai[\\/]claude-code[\\/]bin[\\/]claude\.exe/i);
|
|
23
|
+
if(match)entry=path.join(path.dirname(file),match[0]);
|
|
24
|
+
} catch { /* not a text shim */ }
|
|
25
|
+
if(!entry||!fs.existsSync(entry))continue;
|
|
26
|
+
bin=entry;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const label=[bin,...prefixArgs].join(' ');
|
|
30
|
+
const help=probeCli(bin,[...prefixArgs,'--help'],{env});
|
|
31
|
+
const missing=HELP_FLAGS.filter(f=>!help.out.includes(f));
|
|
32
|
+
if(help.status!==0||missing.length){rejected.push(`${label}: --help ${help.error?'probe error '+help.error:'exit '+help.status}${missing.length?'; missing '+missing.join(' '):''}`);continue;}
|
|
33
|
+
const versionResult=probeCli(bin,[...prefixArgs,'--version'],{env});
|
|
34
|
+
const version=/^(\d+\.\d+\.\d+)/.exec(versionResult.stdout.trim())?.[1];
|
|
35
|
+
if(versionResult.status!==0||!version){rejected.push(`${label}: --version ${versionResult.error?'probe error '+versionResult.error:'exit '+versionResult.status+'; unusable output '+JSON.stringify(versionResult.stdout.trim().slice(0,120))}`);continue;}
|
|
36
|
+
return {agent:'claude',config:{id:'claude',label:'Claude Code CLI',version,bin,prefixArgs,available:true,native:prefixArgs.length===0,completionMarker:'__ORCH_DONE__',modelEnv:'ORCH_MODEL',effortEnv:'ORCH_EFFORT'}};
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`CLI_MISSING: claude; no supported installed Claude Code CLI found. No fallback or automatic install. Tried: ${rejected.join(' | ')||'no candidate found on disk'}`);
|
|
39
|
+
}
|
|
40
|
+
// Base argv for `claude -p` (print mode, non-interactive; the orchestrator has
|
|
41
|
+
// already normalized authorization, so permission prompts are skipped). The
|
|
42
|
+
// prompt is appended by the caller right after -p, mirroring the kimi adapter.
|
|
43
|
+
export function claudeArgs(config,{model,effort}={}) {
|
|
44
|
+
const hasEffort=effort&&String(effort).trim()&&!/\s/.test(String(effort));
|
|
45
|
+
return [...(config.prefixArgs||[]),
|
|
46
|
+
'--dangerously-skip-permissions',
|
|
47
|
+
...(model&&String(model).trim()&&!/\s/.test(String(model))?['--model',String(model).trim()]:[]),
|
|
48
|
+
...(hasEffort?['--effort',String(effort).trim()]:[]),
|
|
49
|
+
'-p'];
|
|
50
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {spawn} from 'node:child_process';
|
|
4
|
+
import {claudeArgs} from './claude-host.mjs';
|
|
5
|
+
import {nativeArgumentLines} from './native-argv.mjs';
|
|
6
|
+
|
|
7
|
+
const quote=(s)=>"'"+String(s).replaceAll("'","''")+"'";
|
|
8
|
+
|
|
9
|
+
// Claude Code cold start can be slower than the 60s codex default; keep the same
|
|
10
|
+
// explicit knob as the kimi adapter.
|
|
11
|
+
export const CLAUDE_DECOMPOSE_TIMEOUT_DEFAULT_MS = 240000;
|
|
12
|
+
export function claudePlannerTimeoutMs(env = process.env) {
|
|
13
|
+
const v = Number(env.ORCH_DECOMPOSE_TIMEOUT_MS);
|
|
14
|
+
return Number.isFinite(v) && v > 0 ? v : CLAUDE_DECOMPOSE_TIMEOUT_DEFAULT_MS;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Planner: `claude -p` prints the final assistant text on stdout in print mode.
|
|
18
|
+
export async function runClaudePlanner(config,prompt,cwd) {
|
|
19
|
+
const args=[...claudeArgs(config,{model:process.env.ORCH_MODEL,effort:process.env.ORCH_EFFORT}),prompt];
|
|
20
|
+
return new Promise((resolve,reject)=>{
|
|
21
|
+
const child=spawn(config.bin,args,{cwd,env:process.env,windowsHide:true,shell:false,stdio:['ignore','pipe','pipe']});
|
|
22
|
+
let raw='',errorText='',timedOut=false;
|
|
23
|
+
const timer=setTimeout(()=>{timedOut=true;child.kill();},claudePlannerTimeoutMs());
|
|
24
|
+
child.stdout.on('data',(b)=>{raw+=b;if(raw.length>8000000)child.kill();});
|
|
25
|
+
child.stderr.on('data',(b)=>{errorText+=b;});
|
|
26
|
+
child.on('error',(e)=>{clearTimeout(timer);reject(e);});
|
|
27
|
+
child.on('exit',(code)=>{
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
try {
|
|
30
|
+
if(timedOut||code!==0)throw new Error(timedOut?'CLAUDE_PLANNER_TIMEOUT':'CLAUDE_PLANNER_FAILED: exit '+code+(errorText?': '+String(errorText).slice(0,300):''));
|
|
31
|
+
const final=String(raw||'').trim();
|
|
32
|
+
if(!final)throw new Error('CLAUDE_PLANNER_EMPTY_RESPONSE'+(errorText?': '+String(errorText).slice(0,300):''));
|
|
33
|
+
resolve({final});
|
|
34
|
+
} catch(e){reject(e);}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function writeClaudeLauncher(config,{key,prompt,suffix,workspace,temp}) {
|
|
40
|
+
const stem=String(key+'-'+suffix).replace(/[^a-zA-Z0-9_-]/g,'-');
|
|
41
|
+
const token=/【本块唯一标识】\s*([A-Za-z0-9][A-Za-z0-9_-]*)/.exec(prompt)?.[1];
|
|
42
|
+
if(!token)throw new Error('Claude window requires a completion token');
|
|
43
|
+
fs.mkdirSync(temp,{recursive:true});
|
|
44
|
+
const lp=path.join(temp,'win-launch-'+stem+'.ps1');
|
|
45
|
+
const pidf=path.join(temp,'window-'+stem+'.pid');
|
|
46
|
+
const pf=path.join(temp,'agent-win-'+stem+'.md');
|
|
47
|
+
const rf=path.join(temp,'win-'+stem+'.result.json');
|
|
48
|
+
const log=path.join(temp,'win-'+stem+'.out.log');
|
|
49
|
+
fs.writeFileSync(pf,prompt,'utf8');
|
|
50
|
+
const args=claudeArgs(config,{model:process.env.ORCH_MODEL,effort:process.env.ORCH_EFFORT});
|
|
51
|
+
const body=[
|
|
52
|
+
"$ErrorActionPreference = 'Continue'",
|
|
53
|
+
'trap { exit 0 }',
|
|
54
|
+
`$PID | Set-Content -LiteralPath ${quote(pidf)} -Encoding UTF8`,
|
|
55
|
+
'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8',
|
|
56
|
+
'$OutputEncoding = [System.Text.Encoding]::UTF8',
|
|
57
|
+
'chcp 65001 | Out-Null',
|
|
58
|
+
'[Console]::InputEncoding = [System.Text.Encoding]::UTF8',
|
|
59
|
+
'Write-Host "============================================="',
|
|
60
|
+
'Write-Host " CLAUDE CODE TASK RUNNING - closes automatically on completion"',
|
|
61
|
+
'Write-Host "============================================="',
|
|
62
|
+
`Set-Location -LiteralPath ${quote(workspace)}`,
|
|
63
|
+
`$env:ORCH_WINDOW = '1'`,
|
|
64
|
+
`$token = ${quote(token)}`,
|
|
65
|
+
`$log = ${quote(log)}`,
|
|
66
|
+
`$resultFile = ${quote(rf)}`,
|
|
67
|
+
"function Write-Result {",
|
|
68
|
+
" param([int]$code,[string]$report)",
|
|
69
|
+
" try {",
|
|
70
|
+
" $json = '{\"__EXIT__\":' + $code + ',\"__DONE__\":true,\"__REPORT__\":' + (ConvertTo-Json $report -Compress) + '}'",
|
|
71
|
+
" $d = Split-Path -Parent $resultFile; if ($d -and -not (Test-Path -LiteralPath $d)) { New-Item -ItemType Directory -Force -Path $d | Out-Null }",
|
|
72
|
+
" Set-Content -LiteralPath $resultFile -Value ($json + \"`r`n__EXIT__=\" + $code) -Encoding UTF8",
|
|
73
|
+
" try { Set-Content -LiteralPath ($resultFile + '.self-closed') -Value '1' -Encoding ASCII } catch { }",
|
|
74
|
+
" } catch { }",
|
|
75
|
+
"}",
|
|
76
|
+
"$p = (Get-Content -Raw -Encoding UTF8 '"+String(pf).replace(/'/g,"''")+"').Trim()",
|
|
77
|
+
"if ([string]::IsNullOrWhiteSpace($p)) { Write-Result 2 ''; exit 0 }",
|
|
78
|
+
"$agentArgs = @(" + args.map(quote).join(',') + ') + @($p)',
|
|
79
|
+
...nativeArgumentLines(),
|
|
80
|
+
`& ${quote(config.bin)} @agentArgs 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.TargetObject } else { $_ } } | Tee-Object -FilePath $log`,
|
|
81
|
+
"$code = $LASTEXITCODE",
|
|
82
|
+
"$report = ''",
|
|
83
|
+
"if (Test-Path -LiteralPath $log) {",
|
|
84
|
+
" $lines = Get-Content -LiteralPath $log -Encoding UTF8 -ErrorAction SilentlyContinue",
|
|
85
|
+
" $hit = $lines | Where-Object { $_.Contains($token) } | Select-Object -Last 1",
|
|
86
|
+
" if ($hit) { $report = $hit } else { $report = ($lines | Select-Object -Last 3) -join \"`n\" }",
|
|
87
|
+
"}",
|
|
88
|
+
"Write-Result $code $report",
|
|
89
|
+
'exit 0',
|
|
90
|
+
].join('\r\n');
|
|
91
|
+
fs.writeFileSync(lp,'\uFEFF'+body,'utf8');
|
|
92
|
+
return {lp,pidf,pf,rf,log};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function waitClaudeResult(rf,timeoutMs,pidf) {
|
|
96
|
+
const end=Date.now()+timeoutMs;
|
|
97
|
+
while(Date.now()<end) {
|
|
98
|
+
if(fs.existsSync(rf)) {
|
|
99
|
+
try {
|
|
100
|
+
const r=JSON.parse(fs.readFileSync(rf,'utf8').replace(/^\uFEFF/,'').split(/\r?\n/)[0]);
|
|
101
|
+
return {done:true,exitCode:r.__EXIT__,text:r.__REPORT__||''};
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
if(fs.existsSync(pidf)) {
|
|
105
|
+
const pid=Number(fs.readFileSync(pidf,'utf8').replace(/^\uFEFF/,''));
|
|
106
|
+
try{process.kill(pid,0);}catch{return {done:true,exitCode:1,text:'Claude launcher exited without result'};}
|
|
107
|
+
}
|
|
108
|
+
await new Promise((r)=>setTimeout(r,300));
|
|
109
|
+
}
|
|
110
|
+
return {done:false,exitCode:null,text:'Claude completion timeout'};
|
|
111
|
+
}
|