@brawnen/agent-harness-cli 0.1.1 → 0.1.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.
@@ -0,0 +1,1384 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const HOST_LAYOUT_VERSION = "0.1.2";
5
+ export const HOST_LAYOUT_RULES_MODE = "converged";
6
+
7
+ export const HOST_LAYOUT_HOSTS = ["codex", "claude-code", "gemini-cli"];
8
+
9
+ const SOURCE_ROOT = path.posix.join(".harness");
10
+ const HOSTS_ROOT = path.posix.join(SOURCE_ROOT, "hosts");
11
+ const RULES_ROOT = path.posix.join(SOURCE_ROOT, "rules");
12
+ const GENERATED_ROOT = path.posix.join(SOURCE_ROOT, "generated");
13
+
14
+ const RULE_TARGETS = {
15
+ codex: "AGENTS.md",
16
+ "claude-code": "CLAUDE.md",
17
+ "gemini-cli": "GEMINI.md"
18
+ };
19
+
20
+ const COPY_TARGETS = {
21
+ codex: [
22
+ { source: path.posix.join(HOSTS_ROOT, "codex", "config.toml"), target: ".codex/config.toml" },
23
+ { source: path.posix.join(HOSTS_ROOT, "codex", "hooks.json"), target: ".codex/hooks.json" }
24
+ ],
25
+ "claude-code": [
26
+ { source: path.posix.join(HOSTS_ROOT, "claude", "settings.json"), target: ".claude/settings.json" }
27
+ ],
28
+ "gemini-cli": [
29
+ { source: path.posix.join(HOSTS_ROOT, "gemini", "settings.json"), target: ".gemini/settings.json" }
30
+ ]
31
+ };
32
+
33
+ const MANIFEST_TARGET = path.posix.join(GENERATED_ROOT, "manifest.json");
34
+
35
+ const DEFAULT_SHARED_RULES = `# Harness Kernel Rules
36
+
37
+ 本项目使用 Harness 协议约束 agent 行为。以下规则为强制执行项。
38
+
39
+ ## 1. Current Work-Unit Convergence
40
+
41
+ 每次收到新输入时,agent 必须先收敛当前 \`work unit\`,至少明确以下字段:
42
+
43
+ - \`intent\`
44
+ - \`goal\`
45
+ - \`scope\`
46
+ - \`acceptance\`
47
+ - \`constraints\`
48
+ - \`assumptions\`
49
+
50
+ 在字段未闭合前,不得直接进入写入或执行阶段。
51
+
52
+ 这里的 \`work unit\` 指 agent 当前准备推进的那一段实际工作,不要求先在 prompt 层定义正式 \`task\`。
53
+
54
+ \`next_action\` 判断原则:
55
+
56
+ - 字段已闭合且无阻断问题 -> \`plan\` 或 \`execute\`
57
+ - 可先通过阅读代码、状态或上下文收敛边界 -> \`observe\`
58
+ - 输入只是当前任务内的简短回复或步骤选择 -> \`observe\`
59
+ - 只有存在真实阻断缺口时才 \`clarify\`
60
+
61
+ 只有在需要持久化、恢复、审计,或进入 \`verify / report / delivery\` 时,runtime 才需要把该 \`work unit\` 落成真正的 \`task state\`。
62
+
63
+ ## 2. Clarify
64
+
65
+ 只在以下情况追问用户,且每次只问一个最高价值问题:
66
+
67
+ 1. \`scope\` 不清,可能越界
68
+ 2. \`acceptance\` 无法判断
69
+ 3. 存在高成本路径分叉,需要用户决策
70
+ 4. 命中高风险区域,需确认
71
+ 5. 任务依赖外部资源或权限
72
+
73
+ 可以通过阅读代码自行确认的技术细节,不应追问用户。
74
+
75
+ ## 3. Execute Gate
76
+
77
+ 以下情况禁止直接执行工具调用或修改文件:
78
+
79
+ 1. \`intent / goal / scope / acceptance\` 未闭合
80
+ 2. 当前任务处于 \`needs_clarification\`
81
+ 3. 动作明显超出已确认的 \`scope\`
82
+ 4. 命中高风险范围但未获确认
83
+ 5. 存在未处理的阻断问题
84
+
85
+ 若命中门禁:
86
+
87
+ - 停止当前动作
88
+ - 说明阻断原因
89
+ - 只提出当前最高价值的缺口
90
+
91
+ ## 4. Completion Gate
92
+
93
+ 以下情况禁止宣称任务完成:
94
+
95
+ - 必需 evidence 未产生
96
+ - \`acceptance\` 与结果不匹配
97
+ - 仍存在未关闭的阻断问题
98
+
99
+ 最低验证要求:
100
+
101
+ | intent | 最低要求 |
102
+ |---|---|
103
+ | \`bug\` | 至少一条命令或测试证明问题不再复现 |
104
+ | \`feature\` | 至少一条命令或验证动作证明新能力可运行 |
105
+ | \`refactor\` | 至少一条测试证明行为未破坏 |
106
+ | \`explore\` | 必须给出结论、依据、风险与下一步建议 |
107
+ | \`prototype\` | 可无强制验证,但必须明确标注未验证范围 |
108
+
109
+ ## 5. Observe
110
+
111
+ 当 \`next_action\` 为 \`observe\` 时:
112
+
113
+ - 只允许只读动作
114
+ - 禁止修改文件和其他有副作用的动作
115
+ - observe 结束后必须重新判断 \`next_action\`
116
+
117
+ ## 6. Override
118
+
119
+ 用户可以显式要求跳过部分门禁。
120
+
121
+ 可跳过:
122
+
123
+ - \`clarify\`
124
+ - 非强制验证要求
125
+ - 高风险确认提示
126
+
127
+ 不可跳过:
128
+
129
+ - \`protected_paths\` 写入限制
130
+ - 文件系统或平台硬权限限制
131
+
132
+ 使用 override 时,必须明确标注被跳过的门禁和当前风险。
133
+
134
+ ## 7. Multi-task
135
+
136
+ - 新输入默认先判断是否属于当前活跃任务
137
+ - 明显新任务应新建并挂起旧任务
138
+ - 若只是对上轮问题的回答、步骤选择或简短确认,默认视为当前任务续接
139
+ - 无法确定时,先澄清任务归属
140
+ - 切换任务前必须保存当前任务状态
141
+ `;
142
+
143
+ const DEFAULT_RULE_DELTAS = {
144
+ codex: `## Codex 宿主说明
145
+
146
+ > **注意**:当前项目通过根目录 \`.codex/config.toml\` 与 \`.codex/hooks.json\` 暴露 Codex 宿主入口,但真实源文件位于 \`.harness/hosts/codex/\`。即便有 hook,执行门禁仍不能只依赖 hook,本规则(L2)依旧有效。
147
+
148
+ ### Codex 自动模式
149
+
150
+ 当前项目已提供 repo-local Codex hooks:
151
+
152
+ - \`SessionStart\`:恢复 active task 摘要
153
+ - \`UserPromptSubmit\`:自动 intake / continue / clarify / override
154
+
155
+ 当前默认只启用最小 hook 集合。工具级 \`PreToolUse / PostToolUse\` 虽保留实现,但默认关闭,以降低宿主前台噪音。
156
+
157
+ 建议在当前仓库中使用:
158
+
159
+ - \`codex\`
160
+ - \`codex exec ...\`
161
+
162
+ 若项目未被 Codex 视为 trusted project,仍可显式使用:
163
+
164
+ - \`codex --enable codex_hooks\`
165
+ - \`codex exec --enable codex_hooks ...\`
166
+ `,
167
+ "claude-code": `## Claude Code 宿主说明
168
+
169
+ > **注意**:当前项目通过根目录 \`.claude/settings.json\` 暴露 Claude Code 宿主入口,但真实源文件位于 \`.harness/hosts/claude/\`。即便有 hook,执行门禁仍不能只依赖 hook,本规则(L2)依旧有效。
170
+
171
+ ### Claude Code hook 接入
172
+
173
+ 当前项目已提供:
174
+
175
+ - \`SessionStart\`
176
+ - \`UserPromptSubmit\`
177
+ - \`PreToolUse\`
178
+ - \`PostToolUse\`
179
+ - \`Stop\`
180
+
181
+ ### Claude Code 宿主接入偏好
182
+
183
+ 对于 \`Claude Code\` 宿主接入相关任务:
184
+
185
+ - 默认由 agent 自行完成方案、实现、验证与收口
186
+ - 不要在中途反复向用户确认实现细节
187
+ - 只有在高风险、超出已确认 scope、需要外部权限或不可逆操作时才追问用户
188
+ - 若存在普通实现分支,默认选择实现成本低、验证快、回滚简单的路径直接推进
189
+ `,
190
+ "gemini-cli": `## Gemini CLI 宿主说明
191
+
192
+ > **注意**:当前项目通过根目录 \`.gemini/settings.json\` 暴露 Gemini CLI 宿主入口,但真实源文件位于 \`.harness/hosts/gemini/\`。即便有 hook,执行门禁仍不能只依赖 hook,本规则(L2)依旧有效。
193
+
194
+ ### Gemini CLI hook 接入
195
+
196
+ 当前项目已提供:
197
+
198
+ - \`SessionStart\`
199
+ - \`BeforeAgent\`
200
+ - \`BeforeTool\`
201
+ - \`AfterTool\`
202
+ - \`AfterAgent\`
203
+
204
+ ### Gemini CLI 宿主接入偏好
205
+
206
+ 对于 \`Gemini CLI\` 宿主接入相关任务:
207
+
208
+ - 默认由 agent 自行完成方案、实现、验证与收口
209
+ - 不要在中途反复向用户确认实现细节
210
+ - 只有在高风险、超出已确认 scope、需要外部权限或不可逆操作时才追问用户
211
+ - 若存在普通实现分支,默认选择实现成本低、验证快、回滚简单的路径直接推进
212
+ `
213
+ };
214
+
215
+ const DEFAULT_CLAUDE_SETTINGS = `{
216
+ "hooks": {
217
+ "SessionStart": [
218
+ {
219
+ "matcher": "startup|resume|clear|compact",
220
+ "hooks": [
221
+ {
222
+ "type": "command",
223
+ "command": "node \\"$CLAUDE_PROJECT_DIR/.harness/hosts/claude/hooks/session_start.js\\""
224
+ }
225
+ ]
226
+ }
227
+ ],
228
+ "UserPromptSubmit": [
229
+ {
230
+ "hooks": [
231
+ {
232
+ "type": "command",
233
+ "command": "node \\"$CLAUDE_PROJECT_DIR/.harness/hosts/claude/hooks/user_prompt_submit.js\\""
234
+ }
235
+ ]
236
+ }
237
+ ],
238
+ "PreToolUse": [
239
+ {
240
+ "matcher": "Write|Edit|Bash|NotebookEdit",
241
+ "hooks": [
242
+ {
243
+ "type": "command",
244
+ "command": "node \\"$CLAUDE_PROJECT_DIR/.harness/hosts/claude/hooks/pre_tool_use.js\\""
245
+ }
246
+ ]
247
+ }
248
+ ],
249
+ "PostToolUse": [
250
+ {
251
+ "matcher": ".*",
252
+ "hooks": [
253
+ {
254
+ "type": "command",
255
+ "command": "node \\"$CLAUDE_PROJECT_DIR/.harness/hosts/claude/hooks/post_tool_use.js\\""
256
+ }
257
+ ]
258
+ }
259
+ ],
260
+ "Stop": [
261
+ {
262
+ "hooks": [
263
+ {
264
+ "type": "command",
265
+ "command": "node \\"$CLAUDE_PROJECT_DIR/.harness/hosts/claude/hooks/stop.js\\""
266
+ }
267
+ ]
268
+ }
269
+ ]
270
+ }
271
+ }
272
+ `;
273
+
274
+ const DEFAULT_GEMINI_SETTINGS = `{
275
+ "hooks": {
276
+ "SessionStart": [
277
+ {
278
+ "matcher": "*",
279
+ "hooks": [
280
+ {
281
+ "type": "command",
282
+ "command": "node \\"$(git rev-parse --show-toplevel)/.harness/hosts/gemini/hooks/session_start.js\\""
283
+ }
284
+ ]
285
+ }
286
+ ],
287
+ "BeforeAgent": [
288
+ {
289
+ "matcher": "*",
290
+ "hooks": [
291
+ {
292
+ "type": "command",
293
+ "command": "node \\"$(git rev-parse --show-toplevel)/.harness/hosts/gemini/hooks/before_agent.js\\""
294
+ }
295
+ ]
296
+ }
297
+ ],
298
+ "BeforeTool": [
299
+ {
300
+ "matcher": "run_shell_command|write_file|replace",
301
+ "hooks": [
302
+ {
303
+ "type": "command",
304
+ "command": "node \\"$(git rev-parse --show-toplevel)/.harness/hosts/gemini/hooks/before_tool.js\\""
305
+ }
306
+ ]
307
+ }
308
+ ],
309
+ "AfterTool": [
310
+ {
311
+ "matcher": "run_shell_command|write_file|replace",
312
+ "hooks": [
313
+ {
314
+ "type": "command",
315
+ "command": "node \\"$(git rev-parse --show-toplevel)/.harness/hosts/gemini/hooks/after_tool.js\\""
316
+ }
317
+ ]
318
+ }
319
+ ],
320
+ "AfterAgent": [
321
+ {
322
+ "matcher": "*",
323
+ "hooks": [
324
+ {
325
+ "type": "command",
326
+ "command": "node \\"$(git rev-parse --show-toplevel)/.harness/hosts/gemini/hooks/after_agent.js\\""
327
+ }
328
+ ]
329
+ }
330
+ ]
331
+ }
332
+ }
333
+ `;
334
+
335
+ const DEFAULT_SHARED_PAYLOAD_IO = `import fs from "node:fs";
336
+
337
+ export function readHookPayload() {
338
+ const raw = fs.readFileSync(0, "utf8").trim();
339
+ if (!raw) {
340
+ return {};
341
+ }
342
+
343
+ try {
344
+ return JSON.parse(raw);
345
+ } catch {
346
+ throw new Error("hook stdin 不是合法 JSON");
347
+ }
348
+ }
349
+
350
+ export function resolvePayloadCwd(payload) {
351
+ if (typeof payload?.cwd === "string" && payload.cwd.trim()) {
352
+ return payload.cwd.trim();
353
+ }
354
+
355
+ return process.cwd();
356
+ }
357
+
358
+ export function resolvePayloadPrompt(payload) {
359
+ if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
360
+ return payload.prompt.trim();
361
+ }
362
+
363
+ if (typeof payload?.user_prompt === "string" && payload.user_prompt.trim()) {
364
+ return payload.user_prompt.trim();
365
+ }
366
+
367
+ return "";
368
+ }
369
+
370
+ export function firstString(values) {
371
+ for (const value of values) {
372
+ if (typeof value === "string" && value.trim().length > 0) {
373
+ return value.trim();
374
+ }
375
+ }
376
+
377
+ return null;
378
+ }
379
+
380
+ export function firstDefined(values) {
381
+ for (const value of values) {
382
+ if (value !== undefined && value !== null) {
383
+ return value;
384
+ }
385
+ }
386
+
387
+ return null;
388
+ }
389
+
390
+ export function writeHookOutput(result) {
391
+ process.stdout.write(\`\${JSON.stringify(result, null, 2)}\\n\`);
392
+ }
393
+ `;
394
+
395
+ const DEFAULT_SHARED_RUNTIME_LOADER = `import fs from "node:fs";
396
+ import path from "node:path";
397
+ import { createRequire } from "node:module";
398
+ import { pathToFileURL } from "node:url";
399
+
400
+ const require = createRequire(import.meta.url);
401
+
402
+ const RUNTIME_MODULES = {
403
+ "runtime-host": {
404
+ monorepoPath: ["packages", "cli", "src", "runtime-host", "index.js"],
405
+ packageFilePath: ["node_modules", "@brawnen", "agent-harness-cli", "src", "runtime-host", "index.js"],
406
+ packageSpecifier: "@brawnen/agent-harness-cli/runtime-host"
407
+ }
408
+ };
409
+
410
+ export async function importRuntimeModule(moduleName, cwd = process.cwd()) {
411
+ const definition = RUNTIME_MODULES[String(moduleName ?? "").trim()];
412
+ if (!definition) {
413
+ throw new Error(\`未知 runtime 模块:\${moduleName}\`);
414
+ }
415
+
416
+ const repoRoot = resolveRepoRoot(cwd);
417
+ const candidates = [
418
+ path.join(repoRoot, ...definition.monorepoPath),
419
+ path.join(repoRoot, ...definition.packageFilePath)
420
+ ];
421
+
422
+ for (const candidate of candidates) {
423
+ if (fs.existsSync(candidate)) {
424
+ return import(pathToFileURL(candidate).href);
425
+ }
426
+ }
427
+
428
+ try {
429
+ const resolved = require.resolve(definition.packageSpecifier, {
430
+ paths: [repoRoot]
431
+ });
432
+ return import(pathToFileURL(resolved).href);
433
+ } catch {
434
+ throw new Error(
435
+ \`无法解析 agent-harness runtime 模块:\${moduleName}。请确认目标仓库已安装 @brawnen/agent-harness-cli,或当前在 agent-harness monorepo 内执行。\`
436
+ );
437
+ }
438
+ }
439
+
440
+ export async function importCliModule(moduleRelativePath, cwd = process.cwd()) {
441
+ const repoRoot = resolveRepoRoot(cwd);
442
+ const normalizedPath = String(moduleRelativePath ?? "").replace(/^[/\\\\]+/, "");
443
+ const candidates = [
444
+ path.join(repoRoot, "packages", "cli", normalizedPath),
445
+ path.join(repoRoot, "node_modules", "@brawnen", "agent-harness-cli", normalizedPath)
446
+ ];
447
+
448
+ for (const candidate of candidates) {
449
+ if (fs.existsSync(candidate)) {
450
+ return import(pathToFileURL(candidate).href);
451
+ }
452
+ }
453
+
454
+ try {
455
+ const resolved = require.resolve(\`@brawnen/agent-harness-cli/\${toPosixPath(normalizedPath)}\`, {
456
+ paths: [repoRoot]
457
+ });
458
+ return import(pathToFileURL(resolved).href);
459
+ } catch {
460
+ throw new Error(
461
+ \`无法解析 agent-harness runtime 模块:\${normalizedPath}。请确认目标仓库已安装 @brawnen/agent-harness-cli,或当前在 agent-harness monorepo 内执行。\`
462
+ );
463
+ }
464
+ }
465
+
466
+ function resolveRepoRoot(cwd) {
467
+ let current = path.resolve(cwd);
468
+
469
+ while (true) {
470
+ if (
471
+ fs.existsSync(path.join(current, "harness.yaml")) ||
472
+ fs.existsSync(path.join(current, ".harness")) ||
473
+ fs.existsSync(path.join(current, ".git"))
474
+ ) {
475
+ return current;
476
+ }
477
+
478
+ const parent = path.dirname(current);
479
+ if (parent === current) {
480
+ return path.resolve(cwd);
481
+ }
482
+ current = parent;
483
+ }
484
+ }
485
+
486
+ function toPosixPath(value) {
487
+ return value.split(path.sep).join(path.posix.sep);
488
+ }
489
+ `;
490
+
491
+ const DEFAULT_CODEX_CONFIG = `[features]
492
+ codex_hooks = true
493
+ `;
494
+
495
+ const DEFAULT_CODEX_HOOKS_JSON = `{
496
+ "$comment": "Repo-local Codex hooks generated from .harness/hosts/codex.",
497
+ "hooks": {
498
+ "UserPromptSubmit": [
499
+ {
500
+ "hooks": [
501
+ {
502
+ "type": "command",
503
+ "command": "node \\"$(git rev-parse --show-toplevel)/.harness/hosts/codex/hooks/user_prompt_submit_intake.js\\" || echo \\"{}\\""
504
+ }
505
+ ]
506
+ }
507
+ ],
508
+ "SessionStart": [
509
+ {
510
+ "matcher": "startup|resume",
511
+ "hooks": [
512
+ {
513
+ "type": "command",
514
+ "command": "node \\"$(git rev-parse --show-toplevel)/.harness/hosts/codex/hooks/session_start_restore.js\\" || echo \\"{}\\""
515
+ }
516
+ ]
517
+ }
518
+ ]
519
+ }
520
+ }
521
+ `;
522
+
523
+ const DEFAULT_CODEX_USER_PROMPT = `import { invokeAgentHarnessCodexHook, readHookPayload, writeContinue } from "./shared/codex-hook-io.js";
524
+
525
+ try {
526
+ const payload = readHookPayload();
527
+ process.stdout.write(\`\${JSON.stringify(await invokeAgentHarnessCodexHook("user-prompt-submit", payload), null, 2)}\\n\`);
528
+ } catch (error) {
529
+ await writeContinue("UserPromptSubmit", \`Codex UserPromptSubmit hook 执行失败:\${error.message}\`);
530
+ }
531
+ `;
532
+
533
+ const DEFAULT_CLAUDE_SESSION_START = `import { readHookPayload, resolvePayloadCwd, writeHookOutput } from "../../shared/payload-io.js";
534
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
535
+
536
+ const FALLBACK_COMMANDS = [
537
+ "npx @brawnen/agent-harness-cli state active",
538
+ "npx @brawnen/agent-harness-cli task intake \\"任务描述\\""
539
+ ];
540
+
541
+ try {
542
+ const payload = readHookPayload();
543
+ const cwd = resolvePayloadCwd(payload);
544
+ const { handleSessionStart, buildClaudeHookOutput } = await importRuntimeModule("runtime-host", cwd);
545
+ writeHookOutput(buildClaudeHookOutput("SessionStart", handleSessionStart({
546
+ cwd,
547
+ fallbackCommands: FALLBACK_COMMANDS,
548
+ hostDisplayName: "Claude Code",
549
+ source: payload?.source ?? ""
550
+ })));
551
+ } catch (error) {
552
+ const { buildClaudeHookOutput } = await importRuntimeModule("runtime-host");
553
+ writeHookOutput(buildClaudeHookOutput("SessionStart", {
554
+ additionalContext: \`Claude Code SessionStart hook 执行失败:\${error.message}\`,
555
+ status: "continue"
556
+ }));
557
+ }
558
+ `;
559
+
560
+ const DEFAULT_CLAUDE_USER_PROMPT = `import { readHookPayload, resolvePayloadCwd, resolvePayloadPrompt, writeHookOutput } from "../../shared/payload-io.js";
561
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
562
+
563
+ const FALLBACK_COMMANDS = [
564
+ "npx @brawnen/agent-harness-cli state active",
565
+ "npx @brawnen/agent-harness-cli task intake \\"任务描述\\"",
566
+ "npx @brawnen/agent-harness-cli task suspend-active --reason \\"切换任务\\""
567
+ ];
568
+
569
+ try {
570
+ const payload = readHookPayload();
571
+ const cwd = resolvePayloadCwd(payload);
572
+ const { handlePromptSubmit, buildClaudeHookOutput } = await importRuntimeModule("runtime-host", cwd);
573
+ writeHookOutput(buildClaudeHookOutput("UserPromptSubmit", handlePromptSubmit({
574
+ cwd,
575
+ fallbackCommands: FALLBACK_COMMANDS,
576
+ hostDisplayName: "Claude Code",
577
+ prompt: resolvePayloadPrompt(payload)
578
+ })));
579
+ } catch (error) {
580
+ const { buildClaudeHookOutput } = await importRuntimeModule("runtime-host");
581
+ writeHookOutput(buildClaudeHookOutput("UserPromptSubmit", {
582
+ additionalContext: \`Claude Code UserPromptSubmit hook 执行失败:\${error.message}\`,
583
+ status: "continue"
584
+ }));
585
+ }
586
+ `;
587
+
588
+ const DEFAULT_CLAUDE_PRE_TOOL = `import { firstString, readHookPayload, resolvePayloadCwd, writeHookOutput } from "../../shared/payload-io.js";
589
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
590
+
591
+ try {
592
+ const payload = readHookPayload();
593
+ const cwd = resolvePayloadCwd(payload);
594
+ const { handleBeforeTool, buildClaudeHookOutput } = await importRuntimeModule("runtime-host", cwd);
595
+ const result = handleBeforeTool({
596
+ command: firstString([
597
+ payload?.tool_input?.command,
598
+ payload?.toolInput?.command,
599
+ payload?.input?.command,
600
+ payload?.arguments?.command,
601
+ payload?.tool_use?.input?.command,
602
+ payload?.toolUse?.input?.command,
603
+ payload?.command
604
+ ]) ?? "",
605
+ cwd,
606
+ filePath: firstString([
607
+ payload?.tool_input?.file_path,
608
+ payload?.tool_input?.path,
609
+ payload?.toolInput?.file_path,
610
+ payload?.toolInput?.path,
611
+ payload?.input?.file_path,
612
+ payload?.input?.path,
613
+ payload?.arguments?.file_path,
614
+ payload?.arguments?.path,
615
+ payload?.tool_use?.input?.file_path,
616
+ payload?.tool_use?.input?.path,
617
+ payload?.toolUse?.input?.file_path,
618
+ payload?.toolUse?.input?.path
619
+ ]),
620
+ taskId: firstString([
621
+ payload?.task_id,
622
+ payload?.taskId,
623
+ payload?.context?.task_id,
624
+ payload?.context?.taskId
625
+ ]),
626
+ toolName: firstString([
627
+ payload?.tool_name,
628
+ payload?.toolName,
629
+ payload?.tool?.name,
630
+ payload?.toolUse?.name,
631
+ payload?.name
632
+ ])
633
+ });
634
+ writeHookOutput(buildClaudeHookOutput("PreToolUse", result));
635
+ } catch {
636
+ writeHookOutput({});
637
+ }
638
+ `;
639
+
640
+ const DEFAULT_CLAUDE_POST_TOOL = `import { firstDefined, firstString, readHookPayload, resolvePayloadCwd, writeHookOutput } from "../../shared/payload-io.js";
641
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
642
+
643
+ try {
644
+ const payload = readHookPayload();
645
+ const cwd = resolvePayloadCwd(payload);
646
+ const { appendMinimalToolEvidence } = await importRuntimeModule("runtime-host", cwd);
647
+ appendMinimalToolEvidence({
648
+ cwd,
649
+ exitCode: resolveExitCode(payload) ?? 0,
650
+ toolName: firstString([
651
+ payload?.tool_name,
652
+ payload?.toolName,
653
+ payload?.tool?.name,
654
+ payload?.toolUse?.name,
655
+ payload?.name
656
+ ])
657
+ });
658
+
659
+ writeHookOutput({});
660
+ } catch {
661
+ writeHookOutput({});
662
+ }
663
+
664
+ function resolveExitCode(payload) {
665
+ const value = firstDefined([
666
+ payload?.exit_code,
667
+ payload?.exitCode,
668
+ payload?.result?.exit_code,
669
+ payload?.result?.exitCode,
670
+ payload?.tool_output?.exit_code,
671
+ payload?.toolOutput?.exitCode,
672
+ payload?.status
673
+ ]);
674
+
675
+ if (typeof value === "number") {
676
+ return value;
677
+ }
678
+
679
+ if (typeof value === "string" && value.trim() !== "") {
680
+ const parsed = Number(value);
681
+ return Number.isFinite(parsed) ? parsed : null;
682
+ }
683
+
684
+ return null;
685
+ }
686
+ `;
687
+
688
+ const DEFAULT_CLAUDE_STOP = `import { readHookPayload, resolvePayloadCwd, writeHookOutput } from "../../shared/payload-io.js";
689
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
690
+
691
+ try {
692
+ const payload = readHookPayload();
693
+ const cwd = resolvePayloadCwd(payload);
694
+ const { handleCompletionGate, buildClaudeHookOutput, resolveClaudeCompletionMessage } = await importRuntimeModule("runtime-host", cwd);
695
+ writeHookOutput(buildClaudeHookOutput("Stop", handleCompletionGate({
696
+ cwd,
697
+ lastAssistantMessage: resolveClaudeCompletionMessage(payload)
698
+ })));
699
+ } catch {
700
+ writeHookOutput({});
701
+ }
702
+ `;
703
+
704
+ const DEFAULT_GEMINI_SESSION_START = `import { readHookPayload, resolvePayloadCwd, writeHookOutput } from "../../shared/payload-io.js";
705
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
706
+
707
+ const FALLBACK_COMMANDS = [
708
+ "node packages/cli/bin/agent-harness.js state active",
709
+ "node packages/cli/bin/agent-harness.js task intake \\"任务描述\\""
710
+ ];
711
+
712
+ try {
713
+ const payload = readHookPayload();
714
+ const cwd = resolvePayloadCwd(payload);
715
+ const { handleSessionStart, buildGeminiHookOutput } = await importRuntimeModule("runtime-host", cwd);
716
+ writeHookOutput(buildGeminiHookOutput(handleSessionStart({
717
+ cwd,
718
+ fallbackCommands: FALLBACK_COMMANDS,
719
+ hostDisplayName: "Gemini CLI",
720
+ source: payload?.source ?? ""
721
+ })));
722
+ } catch {
723
+ writeHookOutput({});
724
+ }
725
+ `;
726
+
727
+ const DEFAULT_GEMINI_BEFORE_AGENT = `import { readHookPayload, resolvePayloadCwd, resolvePayloadPrompt, writeHookOutput } from "../../shared/payload-io.js";
728
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
729
+
730
+ const FALLBACK_COMMANDS = [
731
+ "node packages/cli/bin/agent-harness.js state active",
732
+ "node packages/cli/bin/agent-harness.js task intake \\"任务描述\\"",
733
+ "node packages/cli/bin/agent-harness.js task suspend-active --reason \\"切换任务\\""
734
+ ];
735
+
736
+ try {
737
+ const payload = readHookPayload();
738
+ const cwd = resolvePayloadCwd(payload);
739
+ const { handlePromptSubmit, buildGeminiHookOutput } = await importRuntimeModule("runtime-host", cwd);
740
+ writeHookOutput(buildGeminiHookOutput(handlePromptSubmit({
741
+ cwd,
742
+ fallbackCommands: FALLBACK_COMMANDS,
743
+ hostDisplayName: "Gemini CLI",
744
+ prompt: resolvePayloadPrompt(payload)
745
+ })));
746
+ } catch {
747
+ writeHookOutput({});
748
+ }
749
+ `;
750
+
751
+ const DEFAULT_GEMINI_BEFORE_TOOL = `import { readHookPayload, resolvePayloadCwd, writeHookOutput } from "../../shared/payload-io.js";
752
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
753
+
754
+ try {
755
+ const payload = readHookPayload();
756
+ const cwd = resolvePayloadCwd(payload);
757
+ const {
758
+ handleBeforeTool,
759
+ buildGeminiHookOutput,
760
+ resolveGeminiToolCommand,
761
+ resolveGeminiToolName,
762
+ resolveGeminiToolPath
763
+ } = await importRuntimeModule("runtime-host", cwd);
764
+ writeHookOutput(buildGeminiHookOutput(handleBeforeTool({
765
+ command: resolveGeminiToolCommand(payload),
766
+ cwd,
767
+ filePath: resolveGeminiToolPath(payload),
768
+ toolName: resolveGeminiToolName(payload)
769
+ })));
770
+ } catch {
771
+ writeHookOutput({});
772
+ }
773
+ `;
774
+
775
+ const DEFAULT_GEMINI_AFTER_TOOL = `import { readHookPayload, resolvePayloadCwd, writeHookOutput } from "../../shared/payload-io.js";
776
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
777
+
778
+ try {
779
+ const payload = readHookPayload();
780
+ const cwd = resolvePayloadCwd(payload);
781
+ const {
782
+ handleAfterTool,
783
+ buildGeminiHookOutput,
784
+ resolveGeminiToolCommand,
785
+ resolveGeminiToolExitCode,
786
+ resolveGeminiToolName,
787
+ resolveGeminiToolOutput
788
+ } = await importRuntimeModule("runtime-host", cwd);
789
+ writeHookOutput(buildGeminiHookOutput(handleAfterTool({
790
+ command: resolveGeminiToolCommand(payload),
791
+ cwd,
792
+ exitCode: resolveGeminiToolExitCode(payload),
793
+ output: resolveGeminiToolOutput(payload),
794
+ toolName: resolveGeminiToolName(payload)
795
+ })));
796
+ } catch {
797
+ writeHookOutput({});
798
+ }
799
+ `;
800
+
801
+ const DEFAULT_GEMINI_AFTER_AGENT = `import { readHookPayload, resolvePayloadCwd, writeHookOutput } from "../../shared/payload-io.js";
802
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
803
+
804
+ try {
805
+ const payload = readHookPayload();
806
+ const cwd = resolvePayloadCwd(payload);
807
+ const { handleCompletionGate, buildGeminiHookOutput, resolveGeminiCompletionMessage } = await importRuntimeModule("runtime-host", cwd);
808
+ writeHookOutput(buildGeminiHookOutput(handleCompletionGate({
809
+ cwd,
810
+ lastAssistantMessage: resolveGeminiCompletionMessage(payload)
811
+ })));
812
+ } catch {
813
+ writeHookOutput({});
814
+ }
815
+ `;
816
+
817
+ const DEFAULT_CODEX_SESSION_START = `import { invokeAgentHarnessCodexHook, readHookPayload, writeContinue } from "./shared/codex-hook-io.js";
818
+
819
+ try {
820
+ const payload = readHookPayload();
821
+ process.stdout.write(\`\${JSON.stringify(await invokeAgentHarnessCodexHook("session-start", payload), null, 2)}\\n\`);
822
+ } catch (error) {
823
+ await writeContinue("SessionStart", \`Codex SessionStart hook 执行失败:\${error.message}\`);
824
+ }
825
+ `;
826
+
827
+ const DEFAULT_CODEX_PRE_TOOL = `import { firstString, readHookPayload, resolvePayloadCwd } from "../../shared/payload-io.js";
828
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
829
+
830
+ try {
831
+ const payload = readHookPayload();
832
+ const cwd = resolvePayloadCwd(payload);
833
+ const { handleBeforeTool, buildCodexHookOutput } = await importRuntimeModule("runtime-host", cwd);
834
+ const result = handleBeforeTool({
835
+ command: resolveCommand(payload),
836
+ cwd,
837
+ filePath: resolveFilePath(payload),
838
+ taskId: resolveTaskId(payload),
839
+ toolName: resolveToolName(payload)
840
+ });
841
+ process.stdout.write(\`\${JSON.stringify(buildCodexHookOutput("PreToolUse", result), null, 2)}\\n\`);
842
+ } catch {
843
+ process.stdout.write("{}\\n");
844
+ }
845
+
846
+ function resolveToolName(payload) {
847
+ return firstString([
848
+ payload?.tool_name,
849
+ payload?.toolName,
850
+ payload?.tool?.name,
851
+ payload?.toolUse?.name,
852
+ payload?.name
853
+ ]);
854
+ }
855
+
856
+ function resolveFilePath(payload) {
857
+ return firstString([
858
+ payload?.tool_input?.file_path,
859
+ payload?.tool_input?.path,
860
+ payload?.toolInput?.file_path,
861
+ payload?.toolInput?.path,
862
+ payload?.input?.file_path,
863
+ payload?.input?.path,
864
+ payload?.arguments?.file_path,
865
+ payload?.arguments?.path,
866
+ payload?.tool_use?.input?.file_path,
867
+ payload?.tool_use?.input?.path,
868
+ payload?.toolUse?.input?.file_path,
869
+ payload?.toolUse?.input?.path
870
+ ]);
871
+ }
872
+
873
+ function resolveTaskId(payload) {
874
+ return firstString([
875
+ payload?.task_id,
876
+ payload?.taskId,
877
+ payload?.context?.task_id,
878
+ payload?.context?.taskId
879
+ ]);
880
+ }
881
+
882
+ function resolveCommand(payload) {
883
+ return firstString([
884
+ payload?.tool_input?.command,
885
+ payload?.toolInput?.command,
886
+ payload?.input?.command,
887
+ payload?.arguments?.command,
888
+ payload?.tool_use?.input?.command,
889
+ payload?.toolUse?.input?.command,
890
+ payload?.command
891
+ ]);
892
+ }
893
+
894
+ `;
895
+
896
+ const DEFAULT_CODEX_POST_TOOL = `import { firstDefined, firstString, readHookPayload, resolvePayloadCwd } from "../../shared/payload-io.js";
897
+ import { importRuntimeModule } from "../../shared/runtime-loader.js";
898
+
899
+ try {
900
+ const payload = readHookPayload();
901
+ const cwd = resolvePayloadCwd(payload);
902
+ const { handleAfterTool, buildCodexHookOutput } = await importRuntimeModule("runtime-host", cwd);
903
+ const result = handleAfterTool({
904
+ command: resolveCommand(payload),
905
+ cwd,
906
+ exitCode: resolveExitCode(payload),
907
+ output: resolveOutput(payload),
908
+ toolName: "Bash"
909
+ });
910
+ process.stdout.write(\`\${JSON.stringify(buildCodexHookOutput("PostToolUse", result), null, 2)}\\n\`);
911
+ } catch {
912
+ process.stdout.write("{}\\n");
913
+ }
914
+
915
+ function resolveCommand(payload) {
916
+ return firstString([
917
+ payload?.tool_input?.command,
918
+ payload?.toolInput?.command,
919
+ payload?.input?.command,
920
+ payload?.arguments?.command,
921
+ payload?.tool_use?.input?.command,
922
+ payload?.toolUse?.input?.command,
923
+ payload?.command
924
+ ]) ?? "<unknown command>";
925
+ }
926
+
927
+ function resolveExitCode(payload) {
928
+ const value = firstDefined([
929
+ payload?.exit_code,
930
+ payload?.exitCode,
931
+ payload?.result?.exit_code,
932
+ payload?.result?.exitCode,
933
+ payload?.tool_output?.exit_code,
934
+ payload?.toolOutput?.exitCode,
935
+ payload?.status
936
+ ]);
937
+
938
+ if (typeof value === "number") {
939
+ return value;
940
+ }
941
+
942
+ if (typeof value === "string" && value.trim() !== "") {
943
+ const parsed = Number(value);
944
+ return Number.isFinite(parsed) ? parsed : null;
945
+ }
946
+
947
+ return null;
948
+ }
949
+
950
+ function resolveOutput(payload) {
951
+ return firstString([
952
+ payload?.tool_response,
953
+ payload?.output,
954
+ payload?.stdout,
955
+ payload?.stderr,
956
+ payload?.result?.output,
957
+ payload?.result?.stdout,
958
+ payload?.result?.stderr,
959
+ payload?.tool_output?.output,
960
+ payload?.toolOutput?.output
961
+ ]) ?? "";
962
+ }
963
+
964
+ `;
965
+
966
+ const DEFAULT_CODEX_SHARED_IO = `import fs from "node:fs";
967
+ import { importRuntimeModule } from "../../../shared/runtime-loader.js";
968
+
969
+ export function readHookPayload() {
970
+ const raw = fs.readFileSync(0, "utf8").trim();
971
+ if (!raw) {
972
+ return {};
973
+ }
974
+
975
+ try {
976
+ return JSON.parse(raw);
977
+ } catch {
978
+ throw new Error("hook stdin 不是合法 JSON");
979
+ }
980
+ }
981
+
982
+ export function resolvePayloadCwd(payload) {
983
+ if (typeof payload?.cwd === "string" && payload.cwd.trim()) {
984
+ return payload.cwd.trim();
985
+ }
986
+
987
+ return process.cwd();
988
+ }
989
+
990
+ export function resolvePayloadPrompt(payload) {
991
+ if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
992
+ return payload.prompt.trim();
993
+ }
994
+
995
+ if (typeof payload?.user_prompt === "string" && payload.user_prompt.trim()) {
996
+ return payload.user_prompt.trim();
997
+ }
998
+
999
+ return "";
1000
+ }
1001
+
1002
+ export function buildManualFallbackContext(reason, { commands = [], hostDisplayName = "Codex" } = {}) {
1003
+ const safeReason = typeof reason === "string" && reason.trim()
1004
+ ? reason.trim()
1005
+ : \`\${hostDisplayName} hook 执行失败\`;
1006
+ const fallbackCommands = Array.isArray(commands)
1007
+ ? commands.map((value) => String(value ?? "").trim()).filter(Boolean)
1008
+ : [];
1009
+
1010
+ if (fallbackCommands.length === 0) {
1011
+ return \`\${safeReason},已降级继续。\`;
1012
+ }
1013
+
1014
+ return \`\${safeReason},已降级。手动命令:\${fallbackCommands.join(";")}\`;
1015
+ }
1016
+
1017
+ export async function writeContinue(hookEventName, additionalContext = "") {
1018
+ const { buildCodexHookOutput } = await importRuntimeModule("runtime-host");
1019
+ const text = typeof additionalContext === "string" ? additionalContext.trim() : "";
1020
+ process.stdout.write(\`\${JSON.stringify(
1021
+ buildCodexHookOutput(hookEventName, {
1022
+ additionalContext: text,
1023
+ status: "continue"
1024
+ }),
1025
+ null,
1026
+ 2
1027
+ )}\\n\`);
1028
+ }
1029
+
1030
+ export async function writeBlock(reason) {
1031
+ const { buildCodexHookOutput } = await importRuntimeModule("runtime-host");
1032
+ process.stdout.write(\`\${JSON.stringify(
1033
+ buildCodexHookOutput("Block", { reason, status: "block" }),
1034
+ null,
1035
+ 2
1036
+ )}\\n\`);
1037
+ }
1038
+
1039
+ export async function invokeAgentHarnessCodexHook(event, payload) {
1040
+ const cwd = resolvePayloadCwd(payload);
1041
+ try {
1042
+ const { handlePromptSubmit, handleSessionStart, buildCodexHookOutput } = await importRuntimeModule("runtime-host", cwd);
1043
+
1044
+ if (event === "session-start") {
1045
+ return buildCodexHookOutput("SessionStart", handleSessionStart({
1046
+ cwd,
1047
+ fallbackCommands: SESSION_START_FALLBACK_COMMANDS,
1048
+ hostDisplayName: "Codex",
1049
+ source: payload?.source ?? ""
1050
+ }));
1051
+ }
1052
+
1053
+ if (event === "user-prompt-submit") {
1054
+ return buildCodexHookOutput("UserPromptSubmit", handlePromptSubmit({
1055
+ cwd,
1056
+ fallbackCommands: MANUAL_FALLBACK_COMMANDS,
1057
+ hostDisplayName: "Codex",
1058
+ prompt: resolvePayloadPrompt(payload)
1059
+ }));
1060
+ }
1061
+
1062
+ throw new Error(\`未知 Codex hook 事件: \${event}\`);
1063
+ } catch (error) {
1064
+ const hookEventName = event === "session-start" ? "SessionStart" : "UserPromptSubmit";
1065
+ const fallbackCommands = event === "session-start" ? SESSION_START_FALLBACK_COMMANDS : MANUAL_FALLBACK_COMMANDS;
1066
+ const { buildCodexHookOutput } = await importRuntimeModule("runtime-host", cwd);
1067
+ return buildCodexHookOutput(hookEventName, {
1068
+ additionalContext: buildManualFallbackContext(
1069
+ \`Codex \${hookEventName} hook 执行失败:\${error.message}\`,
1070
+ { commands: fallbackCommands, hostDisplayName: "Codex" }
1071
+ ),
1072
+ status: "continue"
1073
+ });
1074
+ }
1075
+ }
1076
+ `;
1077
+
1078
+ const DEFAULT_SOURCE_FILES = {
1079
+ [path.posix.join(SOURCE_ROOT, "package.json")]: `{
1080
+ "type": "module"
1081
+ }
1082
+ `,
1083
+ [path.posix.join(HOSTS_ROOT, "shared", "payload-io.js")]: DEFAULT_SHARED_PAYLOAD_IO,
1084
+ [path.posix.join(HOSTS_ROOT, "shared", "runtime-loader.js")]: DEFAULT_SHARED_RUNTIME_LOADER,
1085
+ [path.posix.join(HOSTS_ROOT, "codex", "config.toml")]: DEFAULT_CODEX_CONFIG,
1086
+ [path.posix.join(HOSTS_ROOT, "codex", "hooks.json")]: DEFAULT_CODEX_HOOKS_JSON,
1087
+ [path.posix.join(HOSTS_ROOT, "codex", "hooks", "user_prompt_submit_intake.js")]: DEFAULT_CODEX_USER_PROMPT,
1088
+ [path.posix.join(HOSTS_ROOT, "codex", "hooks", "session_start_restore.js")]: DEFAULT_CODEX_SESSION_START,
1089
+ [path.posix.join(HOSTS_ROOT, "codex", "hooks", "pre_tool_use_gate.js")]: DEFAULT_CODEX_PRE_TOOL,
1090
+ [path.posix.join(HOSTS_ROOT, "codex", "hooks", "post_tool_use_record_evidence.js")]: DEFAULT_CODEX_POST_TOOL,
1091
+ [path.posix.join(HOSTS_ROOT, "codex", "hooks", "shared", "codex-hook-io.js")]: DEFAULT_CODEX_SHARED_IO,
1092
+ [path.posix.join(HOSTS_ROOT, "claude", "settings.json")]: DEFAULT_CLAUDE_SETTINGS,
1093
+ [path.posix.join(HOSTS_ROOT, "claude", "hooks", "session_start.js")]: DEFAULT_CLAUDE_SESSION_START,
1094
+ [path.posix.join(HOSTS_ROOT, "claude", "hooks", "user_prompt_submit.js")]: DEFAULT_CLAUDE_USER_PROMPT,
1095
+ [path.posix.join(HOSTS_ROOT, "claude", "hooks", "pre_tool_use.js")]: DEFAULT_CLAUDE_PRE_TOOL,
1096
+ [path.posix.join(HOSTS_ROOT, "claude", "hooks", "post_tool_use.js")]: DEFAULT_CLAUDE_POST_TOOL,
1097
+ [path.posix.join(HOSTS_ROOT, "claude", "hooks", "stop.js")]: DEFAULT_CLAUDE_STOP,
1098
+ [path.posix.join(HOSTS_ROOT, "gemini", "settings.json")]: DEFAULT_GEMINI_SETTINGS,
1099
+ [path.posix.join(HOSTS_ROOT, "gemini", "hooks", "session_start.js")]: DEFAULT_GEMINI_SESSION_START,
1100
+ [path.posix.join(HOSTS_ROOT, "gemini", "hooks", "before_agent.js")]: DEFAULT_GEMINI_BEFORE_AGENT,
1101
+ [path.posix.join(HOSTS_ROOT, "gemini", "hooks", "before_tool.js")]: DEFAULT_GEMINI_BEFORE_TOOL,
1102
+ [path.posix.join(HOSTS_ROOT, "gemini", "hooks", "after_tool.js")]: DEFAULT_GEMINI_AFTER_TOOL,
1103
+ [path.posix.join(HOSTS_ROOT, "gemini", "hooks", "after_agent.js")]: DEFAULT_GEMINI_AFTER_AGENT,
1104
+ [path.posix.join(RULES_ROOT, "shared.md")]: DEFAULT_SHARED_RULES,
1105
+ [path.posix.join(RULES_ROOT, "codex.md")]: DEFAULT_RULE_DELTAS.codex,
1106
+ [path.posix.join(RULES_ROOT, "claude.md")]: DEFAULT_RULE_DELTAS["claude-code"],
1107
+ [path.posix.join(RULES_ROOT, "gemini.md")]: DEFAULT_RULE_DELTAS["gemini-cli"]
1108
+ };
1109
+
1110
+ export function collectHostLayoutWrites(cwd, options = {}) {
1111
+ const hosts = normalizeHosts(options.hosts);
1112
+ const rewrite = options.rewrite === true;
1113
+ const check = options.check === true;
1114
+ const seedMissing = options.seedMissing !== false;
1115
+ const includeConfigs = options.includeConfigs !== false;
1116
+ const includeRules = options.includeRules !== false;
1117
+
1118
+ const writes = [];
1119
+ const warnings = [];
1120
+ const desired = new Map();
1121
+
1122
+ for (const relativePath of requiredSourcePaths(hosts, { includeConfigs, includeRules })) {
1123
+ const targetPath = path.join(cwd, relativePath);
1124
+ const content = `${String(DEFAULT_SOURCE_FILES[relativePath] ?? "").replace(/\s*$/, "")}\n`;
1125
+ desired.set(relativePath, content);
1126
+
1127
+ if (!fs.existsSync(targetPath)) {
1128
+ if (check || !seedMissing) {
1129
+ warnings.push(`缺少布局源文件: ${relativePath}`);
1130
+ continue;
1131
+ }
1132
+
1133
+ writes.push({
1134
+ content,
1135
+ relativePath,
1136
+ targetPath,
1137
+ type: "source"
1138
+ });
1139
+ continue;
1140
+ }
1141
+ }
1142
+
1143
+ for (const host of hosts) {
1144
+ if (includeConfigs) {
1145
+ for (const mapping of COPY_TARGETS[host] ?? []) {
1146
+ const sourceContent = readDesiredContent(cwd, mapping.source, desired);
1147
+ if (sourceContent == null) {
1148
+ warnings.push(`缺少宿主源文件: ${mapping.source}`);
1149
+ continue;
1150
+ }
1151
+
1152
+ queueWriteIfChanged(writes, cwd, mapping.target, sourceContent, "host");
1153
+ }
1154
+ }
1155
+
1156
+ if (includeRules) {
1157
+ const renderedRules = renderRuleDocument(cwd, host, desired);
1158
+ if (!renderedRules) {
1159
+ warnings.push(`缺少规则源文件,无法生成 ${RULE_TARGETS[host]}`);
1160
+ continue;
1161
+ }
1162
+
1163
+ const ruleTarget = RULE_TARGETS[host];
1164
+ const rulePath = path.join(cwd, ruleTarget);
1165
+ const existing = fs.existsSync(rulePath) ? fs.readFileSync(rulePath, "utf8") : "";
1166
+ const nextContent = buildRuleTargetContent(existing, renderedRules, rewrite);
1167
+
1168
+ if (nextContent == null) {
1169
+ warnings.push(`${ruleTarget} 已存在且未受管,使用 sync --rewrite 才会覆盖`);
1170
+ continue;
1171
+ }
1172
+
1173
+ queueWriteIfChanged(writes, cwd, ruleTarget, nextContent, "rule");
1174
+ }
1175
+ }
1176
+
1177
+ if (includeConfigs || includeRules) {
1178
+ const manifestContent = buildManifestContent(cwd, hosts, desired, { includeConfigs, includeRules });
1179
+ queueWriteIfChanged(writes, cwd, MANIFEST_TARGET, manifestContent, "generated");
1180
+ }
1181
+
1182
+ return { warnings, writes };
1183
+ }
1184
+
1185
+ export function applyHostLayoutWrites(writes) {
1186
+ for (const write of writes) {
1187
+ fs.mkdirSync(path.dirname(write.targetPath), { recursive: true });
1188
+ fs.writeFileSync(write.targetPath, write.content, "utf8");
1189
+ }
1190
+ }
1191
+
1192
+ export function hasConvergedHostLayout(cwd) {
1193
+ return fs.existsSync(path.join(cwd, HOSTS_ROOT)) && fs.existsSync(path.join(cwd, RULES_ROOT));
1194
+ }
1195
+
1196
+ function normalizeHosts(hosts) {
1197
+ const values = Array.isArray(hosts) && hosts.length > 0 ? hosts : HOST_LAYOUT_HOSTS;
1198
+ return values.filter((host) => HOST_LAYOUT_HOSTS.includes(host));
1199
+ }
1200
+
1201
+ function requiredSourcePaths(hosts, options = {}) {
1202
+ const includeConfigs = options.includeConfigs !== false;
1203
+ const includeRules = options.includeRules !== false;
1204
+ const paths = new Set([
1205
+ path.posix.join(SOURCE_ROOT, "package.json"),
1206
+ path.posix.join(HOSTS_ROOT, "shared", "payload-io.js"),
1207
+ path.posix.join(HOSTS_ROOT, "shared", "runtime-loader.js")
1208
+ ]);
1209
+
1210
+ if (includeRules) {
1211
+ paths.add(path.posix.join(RULES_ROOT, "shared.md"));
1212
+ }
1213
+
1214
+ for (const host of hosts) {
1215
+ if (includeConfigs) {
1216
+ for (const mapping of COPY_TARGETS[host] ?? []) {
1217
+ paths.add(mapping.source);
1218
+ }
1219
+ }
1220
+
1221
+ if (host === "codex" && includeConfigs) {
1222
+ paths.add(path.posix.join(HOSTS_ROOT, "codex", "hooks", "user_prompt_submit_intake.js"));
1223
+ paths.add(path.posix.join(HOSTS_ROOT, "codex", "hooks", "session_start_restore.js"));
1224
+ paths.add(path.posix.join(HOSTS_ROOT, "codex", "hooks", "pre_tool_use_gate.js"));
1225
+ paths.add(path.posix.join(HOSTS_ROOT, "codex", "hooks", "post_tool_use_record_evidence.js"));
1226
+ paths.add(path.posix.join(HOSTS_ROOT, "codex", "hooks", "shared", "codex-hook-io.js"));
1227
+ }
1228
+
1229
+ if (host === "claude-code" && includeConfigs) {
1230
+ paths.add(path.posix.join(HOSTS_ROOT, "claude", "hooks", "session_start.js"));
1231
+ paths.add(path.posix.join(HOSTS_ROOT, "claude", "hooks", "user_prompt_submit.js"));
1232
+ paths.add(path.posix.join(HOSTS_ROOT, "claude", "hooks", "pre_tool_use.js"));
1233
+ paths.add(path.posix.join(HOSTS_ROOT, "claude", "hooks", "post_tool_use.js"));
1234
+ paths.add(path.posix.join(HOSTS_ROOT, "claude", "hooks", "stop.js"));
1235
+ }
1236
+
1237
+ if (host === "gemini-cli" && includeConfigs) {
1238
+ paths.add(path.posix.join(HOSTS_ROOT, "gemini", "hooks", "session_start.js"));
1239
+ paths.add(path.posix.join(HOSTS_ROOT, "gemini", "hooks", "before_agent.js"));
1240
+ paths.add(path.posix.join(HOSTS_ROOT, "gemini", "hooks", "before_tool.js"));
1241
+ paths.add(path.posix.join(HOSTS_ROOT, "gemini", "hooks", "after_tool.js"));
1242
+ paths.add(path.posix.join(HOSTS_ROOT, "gemini", "hooks", "after_agent.js"));
1243
+ }
1244
+
1245
+ if (includeRules) {
1246
+ if (host === "codex") {
1247
+ paths.add(path.posix.join(RULES_ROOT, "codex.md"));
1248
+ continue;
1249
+ }
1250
+
1251
+ if (host === "claude-code") {
1252
+ paths.add(path.posix.join(RULES_ROOT, "claude.md"));
1253
+ continue;
1254
+ }
1255
+
1256
+ if (host === "gemini-cli") {
1257
+ paths.add(path.posix.join(RULES_ROOT, "gemini.md"));
1258
+ }
1259
+ }
1260
+ }
1261
+
1262
+ return [...paths];
1263
+ }
1264
+
1265
+ function readDesiredContent(cwd, relativePath, desired) {
1266
+ if (desired.has(relativePath)) {
1267
+ return desired.get(relativePath);
1268
+ }
1269
+
1270
+ const fullPath = path.join(cwd, relativePath);
1271
+ if (!fs.existsSync(fullPath)) {
1272
+ return null;
1273
+ }
1274
+
1275
+ return ensureTrailingNewline(fs.readFileSync(fullPath, "utf8"));
1276
+ }
1277
+
1278
+ function renderRuleDocument(cwd, host, desired) {
1279
+ const shared = readDesiredContent(cwd, path.posix.join(RULES_ROOT, "shared.md"), desired);
1280
+ const deltaName = host === "codex" ? "codex.md" : host === "claude-code" ? "claude.md" : "gemini.md";
1281
+ const delta = readDesiredContent(cwd, path.posix.join(RULES_ROOT, deltaName), desired);
1282
+ if (!shared || !delta) {
1283
+ return null;
1284
+ }
1285
+
1286
+ const body = [shared.trim(), delta.trim()].filter(Boolean).join("\n\n");
1287
+ return ensureTrailingNewline([
1288
+ `<!-- agent-harness:start version="${HOST_LAYOUT_VERSION}" rules="${HOST_LAYOUT_RULES_MODE}" -->`,
1289
+ body,
1290
+ "<!-- agent-harness:end -->"
1291
+ ].join("\n"));
1292
+ }
1293
+
1294
+ function buildRuleTargetContent(existing, renderedRules, rewrite) {
1295
+ if (!existing.trim()) {
1296
+ return renderedRules;
1297
+ }
1298
+
1299
+ if (containsManagedBlock(existing)) {
1300
+ return existing.replace(/<!-- agent-harness:start[\s\S]*?<!-- agent-harness:end -->\n?/m, renderedRules);
1301
+ }
1302
+
1303
+ if (rewrite) {
1304
+ return renderedRules;
1305
+ }
1306
+
1307
+ const prefix = existing.replace(/\s*$/, "");
1308
+ return ensureTrailingNewline(`${prefix}\n\n${renderedRules.trim()}`);
1309
+ }
1310
+
1311
+ function containsManagedBlock(content) {
1312
+ return content.includes("<!-- agent-harness:start") && content.includes("<!-- agent-harness:end -->");
1313
+ }
1314
+
1315
+ function queueWriteIfChanged(writes, cwd, relativePath, content, type) {
1316
+ const targetPath = path.join(cwd, relativePath);
1317
+ const normalized = ensureTrailingNewline(content);
1318
+ const existing = fs.existsSync(targetPath) ? ensureTrailingNewline(fs.readFileSync(targetPath, "utf8")) : null;
1319
+ if (existing === normalized) {
1320
+ return;
1321
+ }
1322
+
1323
+ writes.push({
1324
+ content: normalized,
1325
+ relativePath,
1326
+ targetPath,
1327
+ type
1328
+ });
1329
+ }
1330
+
1331
+ function buildManifestContent(cwd, hosts, desired, options = {}) {
1332
+ const includeConfigs = options.includeConfigs !== false;
1333
+ const includeRules = options.includeRules !== false;
1334
+ const files = [];
1335
+
1336
+ for (const relativePath of requiredSourcePaths(hosts, { includeConfigs, includeRules })) {
1337
+ files.push({
1338
+ path: relativePath,
1339
+ kind: classifyManifestFileKind(relativePath)
1340
+ });
1341
+ }
1342
+
1343
+ for (const host of hosts) {
1344
+ if (includeConfigs) {
1345
+ for (const mapping of COPY_TARGETS[host] ?? []) {
1346
+ files.push({
1347
+ path: mapping.target,
1348
+ kind: "generated_host"
1349
+ });
1350
+ }
1351
+ }
1352
+ if (includeRules) {
1353
+ files.push({
1354
+ path: RULE_TARGETS[host],
1355
+ kind: "generated_rule"
1356
+ });
1357
+ }
1358
+ }
1359
+
1360
+ const manifest = {
1361
+ layout: "converged-host-layout-v1",
1362
+ version: HOST_LAYOUT_VERSION,
1363
+ hosts,
1364
+ files
1365
+ };
1366
+
1367
+ return `${JSON.stringify(manifest, null, 2)}\n`;
1368
+ }
1369
+
1370
+ function ensureTrailingNewline(content) {
1371
+ return `${String(content ?? "").replace(/\s*$/, "")}\n`;
1372
+ }
1373
+
1374
+ function classifyManifestFileKind(relativePath) {
1375
+ if (relativePath.startsWith(HOSTS_ROOT)) {
1376
+ return "host_source";
1377
+ }
1378
+
1379
+ if (relativePath.startsWith(RULES_ROOT)) {
1380
+ return "rule_source";
1381
+ }
1382
+
1383
+ return "layout_source";
1384
+ }