taskforce-loop-engineering 0.9.1 → 0.10.0

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 - 2026-08-11
4
+
5
+ - Add OpenClaw installer language adaptation with `--language auto|en|zh`.
6
+ - Auto-select Chinese for Chinese locales and English for all other or missing locales; explicit language selection takes precedence.
7
+ - Persist the resolved language in queue configuration and installer manifests, and localize generated conversation instructions, worker prompts, progress reports, human-input gates, and terminal notifications.
8
+
3
9
  ## 0.9.1 - 2026-08-11
4
10
 
5
11
  - Recognize explicit `用 loop engineering` requests that ask to align, complete, enhance, develop, or build a system as executable Loop tasks instead of direct chat.
package/README.md CHANGED
@@ -33,6 +33,11 @@ loop-engineering-openclaw-install \
33
33
  --queue agent-tasks
34
34
  ```
35
35
 
36
+ The installer accepts `--language auto|en|zh`. `auto` is the default: Chinese
37
+ locales install Chinese conversation rules and notifications; all other or
38
+ missing locales install English. Use `--language en` or `--language zh` to
39
+ override locale detection explicitly.
40
+
36
41
  The installer reads `openclaw agents list --json` during the plan. Without
37
42
  `--worker-agent`, it chooses an existing `main`, or the only available agent
38
43
  when exactly one exists. If there is no unambiguous choice, it fails with the
@@ -54,12 +59,20 @@ loop-engineering-openclaw-install \
54
59
  --confirm-install
55
60
  ```
56
61
 
57
- The generated dispatcher uses a per-task session key of
62
+ In an English installation, conversation requests can use forms such as:
63
+
64
+ - `Use Loop Engineering to fix this issue.`
65
+ - `Run this through Loop Engineering.`
66
+ - `Queue this only; do not run it yet.`
67
+ - `Continue the current loop with this amendment: ...`
68
+
69
+ Chinese installations provide equivalent Chinese conversation rules and
70
+ examples. The generated dispatcher uses a per-task session key of
58
71
  `agent:<worker-agent>:loop-task-<task-id>` and explicitly marks the task as
59
72
  already loop-managed to prevent recursive re-enqueue. Existing generated files
60
73
  are not overwritten unless `--force` is supplied after review. The installed
61
- conversation policy treats `走 loop` as enqueue plus immediate execution;
62
- `只入队` and `只排队` remain explicit queue-only overrides.
74
+ conversation policy treats an explicit Loop Engineering request as enqueue plus
75
+ immediate execution; explicit queue-only wording remains the override.
63
76
  The confirmed installer also creates and enables a managed per-queue systemd
64
77
  user timer. It wakes the adaptive scheduler once per minute; the persisted
65
78
  scheduler cadence still decides whether work is due. Generated queue configs
@@ -381,7 +394,7 @@ questions from explicit execution handoffs:
381
394
 
382
395
  ```bash
383
396
  loop-engineering route-message \
384
- --message " loop 修复这个问题" \
397
+ --message "Use Loop Engineering to fix this issue" \
385
398
  --queue agent-tasks \
386
399
  --route \
387
400
  --confirm-execute \
@@ -888,11 +901,11 @@ loop-engineering queue-human-decision --config configs/loops/queues/agent-tasks.
888
901
  `run-queue` processes one task. `run-queue-drain` is an explicit batch/daemon
889
902
  command that keeps claiming queued tasks serially until the inbox is empty or
890
903
  `--max-tasks` is reached. The generated OpenClaw conversation wrapper does not
891
- use drain mode: a new `走 loop` request while a task is active supersedes the
904
+ use drain mode: a new explicit Loop Engineering request while a task is active supersedes the
892
905
  active task, records the replacement lineage, stops the old dispatcher process
893
906
  group, and starts the corrected task after the lock is released. Explicit
894
- queue-only wording still creates ordinary queued work. `继续当前 loop,补充要求:…`
895
- uses the amendment path instead: it keeps the same task and worker session,
907
+ queue-only wording still creates ordinary queued work. `Continue the current
908
+ loop with this amendment: ...` uses the amendment path instead: it keeps the same task and worker session,
896
909
  writes `amendments/NNNN.json`, increments `amendment_version` in the task
897
910
  contract, acceptance plan, and dev plan, and requires the worker to reread the
898
911
  latest amendment before each checkpoint and final completion. Both commands use a lease lock so overlapping ticks do not process the same
package/lib/core.mjs CHANGED
@@ -1520,7 +1520,7 @@ export function classifyLoopMessage(message) {
1520
1520
  const mentionsLoop = /(loop engineering|loop-engineering|task-runner|队列|queue|\bloop\b)/i.test(text);
1521
1521
  const statusIntent = mentionsLoop
1522
1522
  && /(查|看|检查|审计|状态|情况|进度|怎么样|为什么|失败|报错|健康|health|status|progress|audit|summar)/i.test(text);
1523
- const executeIntent = /(走\s*loop|继续(?:当前|这个)?\s*loop|给(?:当前|这个)?\s*loop\s*(?:补充|增加|加)|用\s*loop.*(?:解决|执行|完成|处理|修复|对齐|补齐|增强|开发|构建|绕过|避开|跳过|bypass|evade)|丢进.*loop|入队|enqueue|run[- ]?queue|立刻执行|立即执行)/i.test(text);
1523
+ const executeIntent = /(走\s*loop|继续(?:当前|这个)?\s*loop|给(?:当前|这个)?\s*loop\s*(?:补充|增加|加)|用\s*loop.*(?:解决|执行|完成|处理|修复|对齐|补齐|增强|开发|构建|绕过|避开|跳过|bypass|evade)|丢进.*loop|入队|enqueue|run[- ]?queue|立刻执行|立即执行|use\s+(?:loop engineering|the\s+loop)|run\s+(?:this|it|the\s+task).*(?:through|with)\s+(?:loop engineering|the\s+loop)|continue\s+(?:the\s+)?(?:current\s+)?loop|amend\s+(?:the\s+)?(?:current\s+)?loop)/i.test(text);
1524
1524
  const intent = executeIntent ? 'execute' : statusIntent ? 'status' : 'direct';
1525
1525
  return {
1526
1526
  intent,
@@ -1692,9 +1692,32 @@ export async function routeLoopMessage(root, options = {}) {
1692
1692
  };
1693
1693
  }
1694
1694
 
1695
- function terminalNotificationMessage(queue, task) {
1695
+ function normalizeLanguage(value) {
1696
+ return value === 'zh' ? 'zh' : 'en';
1697
+ }
1698
+
1699
+ async function installedQueueLanguage(root, queue, fallback = 'en') {
1700
+ const file = path.join(root, 'configs', 'loops', 'queues', `${queue}.json`);
1701
+ if (!await exists(file)) return normalizeLanguage(fallback);
1702
+ try { return normalizeLanguage((await readJson(file)).language); } catch { return normalizeLanguage(fallback); }
1703
+ }
1704
+
1705
+ function terminalNotificationMessage(queue, task, language = 'en') {
1696
1706
  const needsReview = task.status === 'ready_for_human_review';
1697
1707
  const needsHuman = ['needs_human_input', 'blocked', 'ready_for_human_review'].includes(task.status);
1708
+ if (language === 'zh') return [
1709
+ needsReview ? 'Loop 任务已准备好接受人工验收'
1710
+ : needsHuman ? 'Loop 任务需要人工输入'
1711
+ : 'Loop 任务已到达终态',
1712
+ `任务:${task.title}`,
1713
+ `队列:${queue}`,
1714
+ `状态:${task.status}`,
1715
+ ...(needsReview
1716
+ ? [`下一步:检查最终判定,并为任务 ${task.id} 记录 approve、request_changes 或 reject。`]
1717
+ : needsHuman
1718
+ ? ['下一步:检查任务的最终判定和检查点,解决阻塞,然后明确继续或重新入队。']
1719
+ : [])
1720
+ ].join('\n');
1698
1721
  return [
1699
1722
  needsReview ? 'Loop task is ready for human acceptance'
1700
1723
  : needsHuman ? 'Loop task needs human input'
@@ -1712,6 +1735,7 @@ function terminalNotificationMessage(queue, task) {
1712
1735
 
1713
1736
  export async function notifyTerminalTasks(root, options = {}) {
1714
1737
  const queue = normalizeLoopId(options.queue);
1738
+ const language = await installedQueueLanguage(root, queue, options.language);
1715
1739
  if (!options.notifyCommand && !options.dryRun) {
1716
1740
  throw new Error('queue-terminal-notify requires --notify-command unless --dry-run is used.');
1717
1741
  }
@@ -1739,7 +1763,7 @@ export async function notifyTerminalTasks(root, options = {}) {
1739
1763
  results.push({ taskId: task.id, status: task.status, outcome: 'already_notified', ledger: path.relative(root, ledgerFile) });
1740
1764
  continue;
1741
1765
  }
1742
- const message = terminalNotificationMessage(queue, task);
1766
+ const message = terminalNotificationMessage(queue, task, language);
1743
1767
  if (options.dryRun) {
1744
1768
  results.push({ taskId: task.id, status: task.status, outcome: 'dry_run', message, source: task.source });
1745
1769
  continue;
@@ -1783,11 +1807,19 @@ export async function notifyTerminalTasks(root, options = {}) {
1783
1807
  };
1784
1808
  }
1785
1809
 
1786
- function humanInputMessage(queue, task, checkpoint, gateId) {
1810
+ function humanInputMessage(queue, task, checkpoint, gateId, language = 'en') {
1787
1811
  const blockers = Array.isArray(checkpoint.blockers) ? checkpoint.blockers : [];
1788
1812
  const blockerText = blockers.length
1789
1813
  ? blockers.map((item) => typeof item === 'string' ? item : item?.human_action_required ?? item?.user_action ?? item?.description ?? item?.message ?? JSON.stringify(item))
1790
1814
  : [checkpoint.next_action ?? 'Human input is required before the task can continue.'];
1815
+ if (language === 'zh') return [
1816
+ 'Loop 任务正在等待你的输入',
1817
+ `任务:${task.title}`,
1818
+ `队列:${queue}`,
1819
+ `门禁:${gateId}`,
1820
+ ...blockerText.map((item) => `需要:${item}`),
1821
+ `回复:LOOP ${gateId} <你的输入>`
1822
+ ].join('\n');
1791
1823
  return [
1792
1824
  'Loop task is waiting for your input',
1793
1825
  `task: ${task.title}`,
@@ -1811,6 +1843,7 @@ async function tasksById(root, queue) {
1811
1843
 
1812
1844
  export async function notifyHumanInputRequests(root, options = {}) {
1813
1845
  const queue = normalizeLoopId(options.queue);
1846
+ const language = await installedQueueLanguage(root, queue, options.language);
1814
1847
  if (!options.notifyCommand && !options.dryRun) {
1815
1848
  throw new Error('queue-human-input-notify requires --notify-command unless --dry-run is used.');
1816
1849
  }
@@ -1857,7 +1890,7 @@ export async function notifyHumanInputRequests(root, options = {}) {
1857
1890
  results.push({ taskId, checkpointId, gateId, outcome: gate.status === 'resolved' ? 'resolved' : 'already_notified', ledger: path.relative(root, ledgerFile) });
1858
1891
  continue;
1859
1892
  }
1860
- const message = humanInputMessage(queue, entry.task, checkpoint, gateId);
1893
+ const message = humanInputMessage(queue, entry.task, checkpoint, gateId, language);
1861
1894
  if (options.dryRun) {
1862
1895
  results.push({ taskId, checkpointId, gateId, outcome: 'dry_run', message, source: entry.task.source });
1863
1896
  continue;
@@ -3458,20 +3491,32 @@ function computeQueueSchedulerInterval(previous, policy, observed) {
3458
3491
  };
3459
3492
  }
3460
3493
 
3461
- function summarizeQueueCounts(status) {
3462
- return `queued=${status.queued}, active=${status.active}, failed=${status.failed}, done=${status.done}`;
3494
+ function summarizeQueueCounts(status, language = 'en') {
3495
+ return language === 'zh'
3496
+ ? `排队=${status.queued},执行中=${status.active},失败=${status.failed},完成=${status.done}`
3497
+ : `queued=${status.queued}, active=${status.active}, failed=${status.failed}, done=${status.done}`;
3463
3498
  }
3464
3499
 
3465
3500
  function buildQueueProgressMessage(report) {
3501
+ const language = normalizeLanguage(report.language);
3466
3502
  const lines = [];
3467
3503
  const run = report.observed.runPath ? ` run=${report.observed.runPath}` : '';
3468
3504
  const task = report.observed.taskId ? ` task=${report.observed.taskId}` : '';
3469
- lines.push(`Loop progress: ${report.queue} ${report.status}${task}${run}`);
3470
- lines.push(`Outcome: ${report.outcomeGroup}; next run ${report.nextRunAt}; interval ${report.currentIntervalMs}ms`);
3471
- lines.push(`Before: ${summarizeQueueCounts(report.statusBefore)}`);
3472
- lines.push(`After: ${summarizeQueueCounts(report.statusAfter)}`);
3473
- if (report.reasonSummary) lines.push(`Reason: ${report.reasonSummary}`);
3474
- if (report.attention.length > 0) lines.push(`Needs attention: ${report.attention.join(', ')}`);
3505
+ if (language === 'zh') {
3506
+ lines.push(`Loop 进度:${report.queue} ${report.status}${task}${run}`);
3507
+ lines.push(`结果:${report.outcomeGroup};下次运行 ${report.nextRunAt};间隔 ${report.currentIntervalMs}ms`);
3508
+ lines.push(`之前:${summarizeQueueCounts(report.statusBefore, language)}`);
3509
+ lines.push(`之后:${summarizeQueueCounts(report.statusAfter, language)}`);
3510
+ if (report.reasonSummary) lines.push(`原因:${report.reasonSummary}`);
3511
+ if (report.attention.length > 0) lines.push(`需要关注:${report.attention.join(', ')}`);
3512
+ } else {
3513
+ lines.push(`Loop progress: ${report.queue} ${report.status}${task}${run}`);
3514
+ lines.push(`Outcome: ${report.outcomeGroup}; next run ${report.nextRunAt}; interval ${report.currentIntervalMs}ms`);
3515
+ lines.push(`Before: ${summarizeQueueCounts(report.statusBefore)}`);
3516
+ lines.push(`After: ${summarizeQueueCounts(report.statusAfter)}`);
3517
+ if (report.reasonSummary) lines.push(`Reason: ${report.reasonSummary}`);
3518
+ if (report.attention.length > 0) lines.push(`Needs attention: ${report.attention.join(', ')}`);
3519
+ }
3475
3520
  return lines.join('\n');
3476
3521
  }
3477
3522
 
@@ -3566,6 +3611,7 @@ export async function queueSchedulerTick(root, options) {
3566
3611
  const progressReport = {
3567
3612
  version: 1,
3568
3613
  queue,
3614
+ language: normalizeLanguage(options.language),
3569
3615
  generatedAt: now,
3570
3616
  status,
3571
3617
  outcomeGroup: decision.group,
@@ -3645,6 +3691,9 @@ export async function loadQueueConfig(root, configPath) {
3645
3691
  const file = path.resolve(root, safeRelativePath(configPath, 'queue config'));
3646
3692
  const config = await readJson(file);
3647
3693
  if (config.queue !== undefined) normalizeLoopId(config.queue);
3694
+ if (config.language !== undefined && !['en', 'zh'].includes(config.language)) {
3695
+ throw new Error('queue config language must be en or zh.');
3696
+ }
3648
3697
  if (config.preflightConfig !== undefined) safeRelativePath(config.preflightConfig, 'preflight config');
3649
3698
  if (config.timeoutMs !== undefined && !positiveInteger(config.timeoutMs)) {
3650
3699
  throw new Error('queue config timeoutMs must be a positive integer.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskforce-loop-engineering",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "OpenClaw-native loop engineering CLI, templates, and skill for verifiable agent work loops.",
6
6
  "type": "module",
@@ -31,6 +31,8 @@ const plan = await run(process.execPath, [installer, ...base, '--json']); const
31
31
  const humanPlan = await run(process.execPath, [installer, ...base]); if (humanPlan.code !== 0 || !humanPlan.stdout.includes('Installation confirmation') || !humanPlan.stdout.includes('target platform: Hermes') || !humanPlan.stdout.includes('writes enabled: no (plan only)')) throw new Error('human-readable Hermes confirmation summary missing');
32
32
  const install = await run(process.execPath, [installer, ...base, '--confirm-install', '--json']); if (install.code !== 0 || JSON.parse(install.stdout).status !== 'installed') throw new Error(`Hermes install failed: ${install.stderr}`);
33
33
  const queue = JSON.parse(await readFile(path.join(root, 'configs/loops/queues/hermes-tasks.json'), 'utf8')); if (queue.dispatcher !== 'node scripts/loops/hermes-loop-dispatch.mjs' || queue.scheduler?.required !== true) throw new Error('Hermes queue wiring missing');
34
+ const instructions = await readFile(path.join(root, 'AGENTS.md'), 'utf8'); if (!instructions.includes('Use Loop Engineering to fix this issue') || !instructions.includes('Queue this only; do not run it yet') || /走 loop|只入队|只排队/.test(instructions)) throw new Error('Hermes English conversation instructions are missing or contain Chinese routing examples');
35
+ const wrapper = await readFile(path.join(root, 'scripts/loops/hermes-loop.mjs'), 'utf8'); if (!wrapper.includes('queue') || !wrapper.includes('continue') || /只入队|只排队|继续当前/.test(wrapper)) throw new Error('Hermes English wrapper routing was not fully localized');
34
36
  const dispatcher = await readFile(path.join(root, 'scripts/loops/hermes-loop-dispatch.mjs'), 'utf8'); if (!dispatcher.includes("'-z', prompt")) throw new Error('Hermes one-shot dispatcher missing');
35
37
  const notifier = path.join(root, 'scripts/loops/hermes-loop-notify.mjs'); const notify = await run(process.execPath, [notifier, 'hello from loop'], { env: { ...process.env, HERMES_SEND_CAPTURE: sendCapture, LOOP_NOTIFICATION_SOURCE: JSON.stringify({ channel: 'telegram', target: 'telegram:12345' }) } }); if (notify.code !== 0) throw new Error(`Hermes notifier failed: ${notify.stderr}`);
36
38
  const sent = JSON.parse(await readFile(sendCapture, 'utf8')); if (!sent.includes('telegram:12345') || !sent.includes('hello from loop')) throw new Error('Hermes notifier did not preserve delivery target/message');
@@ -5,13 +5,14 @@ import { spawn } from 'node:child_process';
5
5
  import path from 'node:path';
6
6
 
7
7
  function parseArgs(argv) {
8
- const out = { root: process.cwd(), queue: 'agent-tasks', hermesBin: 'hermes', systemctlBin: 'systemctl', json: false, confirmInstall: false, force: false };
8
+ const out = { root: process.cwd(), queue: 'agent-tasks', hermesBin: 'hermes', systemctlBin: 'systemctl', language: 'auto', json: false, confirmInstall: false, force: false };
9
9
  for (let i = 0; i < argv.length; i++) {
10
10
  const arg = argv[i];
11
11
  if (arg === '--root') out.root = path.resolve(argv[++i]);
12
12
  else if (arg === '--queue') out.queue = argv[++i];
13
13
  else if (arg === '--hermes-bin') out.hermesBin = argv[++i];
14
14
  else if (arg === '--systemctl-bin') out.systemctlBin = argv[++i];
15
+ else if (arg === '--language') out.language = argv[++i];
15
16
  else if (arg === '--confirm-install') out.confirmInstall = true;
16
17
  else if (arg === '--force') out.force = true;
17
18
  else if (arg === '--json') out.json = true;
@@ -21,6 +22,13 @@ function parseArgs(argv) {
21
22
  return out;
22
23
  }
23
24
 
25
+ function resolveLanguage(requested = 'auto', env = process.env) {
26
+ if (!['auto', 'en', 'zh'].includes(requested)) throw new Error('--language must be auto, en, or zh.');
27
+ if (requested !== 'auto') return requested;
28
+ return String(env.LC_ALL || env.LC_MESSAGES || env.LANG || '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
29
+ }
30
+ const text = (language, en, zh) => language === 'zh' ? zh : en;
31
+
24
32
  function safeId(value, label) { if (!/^[a-zA-Z0-9._-]+$/.test(value)) throw new Error(`${label} contains unsupported characters.`); return value; }
25
33
  function systemdEscapePath(value) {
26
34
  return [...Buffer.from(String(value))].map((byte) => /[A-Za-z0-9/_.:-]/.test(String.fromCharCode(byte)) ? String.fromCharCode(byte) : `\\x${byte.toString(16).padStart(2, '0')}`).join('');
@@ -44,7 +52,8 @@ function run(command, args, options = {}) {
44
52
  });
45
53
  }
46
54
 
47
- function formatConfirmationSummary(summary) {
55
+ function formatConfirmationSummary(summary, language) {
56
+ if (language === 'zh') return ['安装确认', ` 目标平台:${summary.targetPlatform}`, ` 平台 CLI:${summary.platformCli}`, ` 工作区:${summary.workspace}`, ` 队列:${summary.queue}`, ` 调度器:${summary.scheduler}`, ` 通知目标:${summary.notificationTarget}`, ` 允许写入:${summary.writesEnabled ? '是' : '否(仅生成计划)'}`].join('\n');
48
57
  return [
49
58
  'Installation confirmation',
50
59
  ` target platform: ${summary.targetPlatform}`,
@@ -57,15 +66,15 @@ function formatConfirmationSummary(summary) {
57
66
  ].join('\n');
58
67
  }
59
68
 
60
- function dispatcherSource({ hermesBin }) {
69
+ function dispatcherSource({ hermesBin, language }) {
70
+ const intro = language === 'zh' ? ['你收到的是一个已经由 Loop Engineering 管理的任务。', '不要再次路由或入队。', '实施前阅读任务合同、开发计划、验收计划和实时补充要求。'] : ['You are receiving an already loop-managed task.', 'Do not route or enqueue this task again.', 'Read the task contract, development plan, acceptance plan, and live amendments before implementation.'];
71
+ const finish = text(language, 'Before each checkpoint and final completion, reread the live amendment file. Write a checkpoint when possible and finish with status, evidence, verification, blockers, and next action.', '每次写检查点和最终完成前都重新读取实时补充要求文件。尽可能写检查点,并以状态、证据、验证、阻塞和下一步结束。');
61
72
  return `#!/usr/bin/env node
62
73
  import { readFile } from 'node:fs/promises';
63
74
  import { spawn } from 'node:child_process';
64
75
  const task = JSON.parse(await readFile(process.env.LOOP_TASK_FILE, 'utf8'));
65
76
  const prompt = [
66
- 'You are receiving an already loop-managed task.',
67
- 'Do not route or enqueue this task again.',
68
- 'Read the task contract, development plan, acceptance plan, and live amendments before implementation.',
77
+ ${intro.map((line) => JSON.stringify(line)).join(',\n ')},
69
78
  \`Task id: \${task.id}\`,
70
79
  \`Task contract: \${process.env.LOOP_TASK_CONTRACT_FILE || 'not provided'}\`,
71
80
  \`Development plan: \${process.env.LOOP_DEV_PLAN_FILE || 'not provided'}\`,
@@ -73,14 +82,16 @@ const prompt = [
73
82
  \`Live amendments: \${process.env.LOOP_LATEST_AMENDMENT_FILE || 'not provided'}\`,
74
83
  \`Checkpoints dir: \${process.env.LOOP_CHECKPOINTS_DIR || 'not provided'}\`,
75
84
  '', task.body, '',
76
- 'Before each checkpoint and final completion, reread the live amendment file. Write a checkpoint when possible and finish with status, evidence, verification, blockers, and next action.'
85
+ ${JSON.stringify(finish)}
77
86
  ].join('\\n');
78
87
  const child = spawn(${JSON.stringify(hermesBin)}, ['-z', prompt], { cwd: process.cwd(), env: process.env, stdio: 'inherit' });
79
88
  child.on('close', (code, signal) => { process.exitCode = code ?? (signal ? 128 : 1); });
80
89
  `;
81
90
  }
82
91
 
83
- function wrapperSource({ queue, loopBin }) {
92
+ function wrapperSource({ queue, loopBin, language }) {
93
+ const amendmentPattern = language === 'zh' ? '(?:继续(?:当前|这个)?\\s*loop|给(?:当前|这个)?\\s*loop\\s*(?:补充|增加|加)|补充当前\\s*loop)' : '(?:continue\\s+(?:the\\s+)?(?:current\\s+)?loop|amend\\s+(?:the\\s+)?(?:current\\s+)?loop|add\\s+(?:this\\s+)?amendment\\s+to\\s+(?:the\\s+)?(?:current\\s+)?loop)';
94
+ const queueOnlyPattern = language === 'zh' ? '(?:只入队|只排队|暂不执行|不立即执行)' : '(?:queue\\s+(?:this|it)\\s+only|only\\s+queue\\s+(?:this|it)|enqueue\\s+(?:this|it)\\s+only|do\\s+not\\s+(?:run|execute)\\s+(?:this|it)\\s+(?:yet|now))';
84
95
  return `#!/usr/bin/env node
85
96
  import { spawn } from 'node:child_process';
86
97
  const [command, ...rest] = process.argv.slice(2);
@@ -89,9 +100,9 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
89
100
  async function runWhenUnlocked(args, waitMs = 300000) { const deadline = Date.now() + waitMs; while (true) { const code = await run(args); if (code !== 2 || Date.now() >= deadline) return code; await wait(250); } }
90
101
  if (command === 'route') {
91
102
  const messageIndex = rest.indexOf('--message'); const message = messageIndex >= 0 ? String(rest[messageIndex + 1] || '') : '';
92
- const amendment = /(?:继续(?:当前|这个)?\\s*loop|给(?:当前|这个)?\\s*loop\\s*(?:补充|增加|加)|补充当前\\s*loop)/i.test(message);
103
+ const amendment = new RegExp(${JSON.stringify(amendmentPattern)}, 'i').test(message);
93
104
  const routeCode = await run(['route-message', '--queue', ${JSON.stringify(queue)}, '--route', '--confirm-execute', amendment ? '--amend-active' : '--supersede-active', ...rest]);
94
- const queueOnly = /(?:只入队|只排队|暂不执行|不立即执行)/.test(message);
105
+ const queueOnly = new RegExp(${JSON.stringify(queueOnlyPattern)}, 'i').test(message);
95
106
  const runCode = routeCode || queueOnly || amendment ? routeCode : await runWhenUnlocked(['run-queue', '--config', ${JSON.stringify(`configs/loops/queues/${queue}.json`)}, '--progress-notify-command', 'node scripts/loops/hermes-loop-notify.mjs']);
96
107
  const humanCode = await run(['queue-human-input-notify', '--queue', ${JSON.stringify(queue)}, '--notify-command', 'node scripts/loops/hermes-loop-notify.mjs']);
97
108
  const terminalCode = await run(['queue-terminal-notify', '--queue', ${JSON.stringify(queue)}, '--notify-command', 'node scripts/loops/hermes-loop-notify.mjs']);
@@ -106,18 +117,18 @@ if (command === 'route') {
106
117
  const humanCode = await run(['queue-human-input-notify', '--queue', ${JSON.stringify(queue)}, '--notify-command', 'node scripts/loops/hermes-loop-notify.mjs']);
107
118
  const terminalCode = await run(['queue-terminal-notify', '--queue', ${JSON.stringify(queue)}, '--notify-command', 'node scripts/loops/hermes-loop-notify.mjs']);
108
119
  process.exitCode = tickCode || humanCode || terminalCode;
109
- } else { console.error('Usage: node scripts/loops/hermes-loop.mjs route|run-once|scheduler-tick'); process.exitCode = 1; }
120
+ } else { console.error(${JSON.stringify(text(language, 'Usage: node scripts/loops/hermes-loop.mjs route|run-once|scheduler-tick', '用法:node scripts/loops/hermes-loop.mjs route|run-once|scheduler-tick'))}); process.exitCode = 1; }
110
121
  `;
111
122
  }
112
123
 
113
- function notifierSource({ hermesBin }) {
124
+ function notifierSource({ hermesBin, language }) {
114
125
  return `#!/usr/bin/env node
115
126
  import { spawn } from 'node:child_process';
116
127
  const message = process.argv.slice(2).join(' ').trim();
117
128
  const rawSource = process.env.LOOP_HUMAN_INPUT_SOURCE || process.env.LOOP_NOTIFICATION_SOURCE || '';
118
- if (!message) { console.error('loop notifier requires a message'); process.exit(2); }
119
- let source; try { source = JSON.parse(rawSource); } catch { console.error('loop notifier received invalid source metadata'); process.exit(2); }
120
- if (!source || typeof source !== 'object' || !source.target) { console.error('Hermes loop notifier requires source.target in platform[:chat[:thread]] form'); process.exit(2); }
129
+ if (!message) { console.error(${JSON.stringify(text(language, 'loop notifier requires a message', 'loop 通知器需要消息参数'))}); process.exit(2); }
130
+ let source; try { source = JSON.parse(rawSource); } catch { console.error(${JSON.stringify(text(language, 'loop notifier received invalid source metadata', 'loop 通知器收到无效的来源元数据'))}); process.exit(2); }
131
+ if (!source || typeof source !== 'object' || !source.target) { console.error(${JSON.stringify(text(language, 'Hermes loop notifier requires source.target in platform[:chat[:thread]] form', 'Hermes loop 通知器要求 source.target 使用 platform[:chat[:thread]] 格式'))}); process.exit(2); }
121
132
  const args = ['send', '--to', String(source.target), '--quiet', message];
122
133
  if (process.env.LOOP_NOTIFICATION_DRY_RUN === '1') { console.log(JSON.stringify({ dryRun: true, command: ${JSON.stringify(hermesBin)}, args })); }
123
134
  else { const child = spawn(${JSON.stringify(hermesBin)}, args, { cwd: process.cwd(), env: process.env, stdio: 'inherit' }); child.on('close', (code, signal) => { process.exitCode = code ?? (signal ? 128 : 1); }); }
@@ -126,11 +137,15 @@ else { const child = spawn(${JSON.stringify(hermesBin)}, args, { cwd: process.cw
126
137
 
127
138
  function schedulerServiceSource({ root, queue }) { return `[Unit]\nDescription=Taskforce Loop Engineering Hermes scheduler for ${queue}\nAfter=default.target\n\n[Service]\nType=oneshot\nWorkingDirectory=${systemdEscapePath(root)}\nExecStart=${systemdEscapePath(process.execPath)} ${systemdEscapePath(path.join(root, 'scripts', 'loops', 'hermes-loop.mjs'))} scheduler-tick --json\n`; }
128
139
  function schedulerTimerSource({ queue }) { return `[Unit]\nDescription=Wake Taskforce Loop Engineering Hermes scheduler for ${queue}\n\n[Timer]\nOnBootSec=30s\nOnUnitActiveSec=1min\nAccuracySec=10s\nPersistent=true\nUnit=hermes-loop-${queue}-scheduler.service\n\n[Install]\nWantedBy=timers.target\n`; }
129
- function instructionsBlock({ queue }) { return `\n<!-- loop-engineering:hermes:start -->\n## Loop Engineering conversation routing (Hermes)\n\n- Route only explicit Loop Engineering requests. Already managed tasks must never be routed again.\n- Run \`node scripts/loops/hermes-loop.mjs route --message "<full user message>"\` and pass \`--source-channel <platform> --source-target <platform:chat_id[:thread_id]>\` so notifications return to the originating Hermes conversation.\n- \`走 loop\` executes immediately; only \`只入队\` or \`只排队\` suppresses execution. External, destructive, production, credential, or memory migration actions remain separately gated.\n- Queue: \`${queue}\`.\n<!-- loop-engineering:hermes:end -->\n`; }
140
+ function instructionsBlock({ queue, language }) {
141
+ if (language === 'zh') return `\n<!-- loop-engineering:hermes:start -->\n## Loop Engineering 会话路由(Hermes)\n\n- 仅路由明确的 Loop Engineering 请求。已管理的任务不能再次路由。\n- 运行 \`node scripts/loops/hermes-loop.mjs route --message "<完整用户消息>"\` 并传入来源 channel 和 target,使通知返回原始 Hermes 会话。\n- \`走 loop\` 立即执行;只有 \`只入队\` 或 \`只排队\` 才不立即执行。高风险动作仍需单独门禁。\n- 队列:\`${queue}\`。\n<!-- loop-engineering:hermes:end -->\n`;
142
+ return `\n<!-- loop-engineering:hermes:start -->\n## Loop Engineering conversation routing (Hermes)\n\n- Route only explicit Loop Engineering requests. Already managed tasks must never be routed again.\n- Run \`node scripts/loops/hermes-loop.mjs route --message "<full user message>"\` and pass \`--source-channel <platform> --source-target <platform:chat_id[:thread_id]>\` so notifications return to the originating Hermes conversation.\n- \`Use Loop Engineering to fix this issue\` and \`Run this through Loop Engineering\` execute immediately. \`Queue this only; do not run it yet\` suppresses execution. \`Continue the current loop with this amendment: ...\` amends the active task.\n- External, destructive, production, credential, or memory migration actions remain separately gated.\n- Queue: \`${queue}\`.\n<!-- loop-engineering:hermes:end -->\n`;
143
+ }
130
144
 
131
145
  async function main() {
132
146
  const args = parseArgs(process.argv.slice(2));
133
- if (args.help) { console.log('Usage: loop-engineering-hermes-install [--root workspace] [--queue agent-tasks] [--hermes-bin hermes] [--systemctl-bin systemctl] [--confirm-install] [--force] [--json]'); return; }
147
+ args.language = resolveLanguage(args.language);
148
+ if (args.help) { console.log('Usage: loop-engineering-hermes-install [--root workspace] [--queue agent-tasks] [--language auto|en|zh] [--hermes-bin hermes] [--systemctl-bin systemctl] [--confirm-install] [--force] [--json]'); return; }
134
149
  safeId(args.queue, 'queue');
135
150
  args.hermesBin = await resolveExecutable(args.hermesBin);
136
151
  const hermes = await run(args.hermesBin, ['--version'], { cwd: args.root });
@@ -140,22 +155,22 @@ async function main() {
140
155
  const unit = `hermes-loop-${args.queue}-scheduler.service`; const timer = `hermes-loop-${args.queue}-scheduler.timer`;
141
156
  const files = { workspaceHealth: path.join(args.root, 'configs/loops/workspace-health.json'), queueConfig: path.join(args.root, 'configs/loops/queues', `${args.queue}.json`), dispatcher: path.join(args.root, 'scripts/loops/hermes-loop-dispatch.mjs'), wrapper: path.join(args.root, 'scripts/loops/hermes-loop.mjs'), notifier: path.join(args.root, 'scripts/loops/hermes-loop-notify.mjs'), manifest: path.join(args.root, 'runtime/loop-engineering-hermes-install.json'), instructions: path.join(args.root, 'AGENTS.md'), schedulerService: path.join(systemdUserDir, unit), schedulerTimer: path.join(systemdUserDir, timer) };
142
157
  const conflicts = []; for (const [kind, file] of Object.entries(files)) if (!['instructions', 'workspaceHealth', 'manifest'].includes(kind) && await exists(file)) conflicts.push(path.relative(args.root, file));
143
- const confirmationSummary = { targetPlatform: 'Hermes', platformCli: args.hermesBin, workspace: args.root, queue: args.queue, scheduler: `systemd user timer ${timer}`, notificationTarget: 'source-bound at runtime (original Hermes conversation)', writesEnabled: args.confirmInstall };
144
- const report = { version: 1, platform: 'hermes', status: args.confirmInstall ? 'installed' : 'plan_only', readOnly: !args.confirmInstall, root: args.root, queue: args.queue, hermesBin: args.hermesBin, hermesVersion: (hermes.stdout || hermes.stderr).trim().slice(0, 200), scheduler: { required: true, unit, timer }, confirmationSummary, files: Object.fromEntries(Object.entries(files).map(([key, file]) => [key, path.relative(args.root, file)])), conflicts };
158
+ const confirmationSummary = { targetPlatform: 'Hermes', platformCli: args.hermesBin, workspace: args.root, queue: args.queue, scheduler: `systemd user timer ${timer}`, notificationTarget: text(args.language, 'source-bound at runtime (original Hermes conversation)', '运行时绑定到原始 Hermes 会话'), writesEnabled: args.confirmInstall };
159
+ const report = { version: 1, platform: 'hermes', language: args.language, status: args.confirmInstall ? 'installed' : 'plan_only', readOnly: !args.confirmInstall, root: args.root, queue: args.queue, hermesBin: args.hermesBin, hermesVersion: (hermes.stdout || hermes.stderr).trim().slice(0, 200), scheduler: { required: true, unit, timer }, confirmationSummary, files: Object.fromEntries(Object.entries(files).map(([key, file]) => [key, path.relative(args.root, file)])), conflicts };
145
160
  if (conflicts.length && !args.force && args.confirmInstall) throw new Error(`Refusing to overwrite: ${conflicts.join(', ')}. Use --force after review.`);
146
- if (!args.json) console.log(formatConfirmationSummary(confirmationSummary));
161
+ if (!args.json) console.log(formatConfirmationSummary(confirmationSummary, args.language));
147
162
  if (args.confirmInstall) {
148
163
  await mkdir(path.dirname(files.queueConfig), { recursive: true }); await mkdir(path.dirname(files.dispatcher), { recursive: true }); await mkdir(path.join(args.root, 'runtime', 'loops', args.queue), { recursive: true });
149
164
  if (!await exists(files.workspaceHealth)) await writeFile(files.workspaceHealth, `${JSON.stringify({ id: 'workspace-health', goal: 'Keep this workspace loop-ready and detect obvious drift.', level: 'L1', mode: 'report-only', maxRuntimeMs: 120000, breaker: { maxConsecutiveFailures: 3, sameFailureThreshold: 2 }, checks: [{ id: 'workspace-root', type: 'files', paths: ['.'] }] }, null, 2)}\n`);
150
- const queueContent = `${JSON.stringify({ queue: args.queue, description: 'Hermes conversation queue.', dispatcher: 'node scripts/loops/hermes-loop-dispatch.mjs', preflightConfig: 'configs/loops/workspace-health.json', timeoutMs: 1800000, leaseMs: 1860000, staleActiveMs: 3600000, scheduler: { required: true, heartbeatMaxAgeMs: 300000, initialInterval: '1m', minInterval: '1m', maxInterval: '4h', speedupFactor: 0.5, backoffFactor: 2, idleBackoffFactor: 2, humanGateBackoffFactor: 3, longRunHeadroomFactor: 1.25, jitter: '10s' }, retry: { maxAttempts: 1, runtimeRecoveryMaxAttempts: 2, sessionMaxTicks: 10, retryDelayMs: 0, retryExitCodes: [1], requiresHumanActionPatterns: ['requires human', '需要人工', 'Permission denied', 'Operation not permitted'] }, revisionPolicy: { enabled: true, maxRevisionRounds: 3, sameFailureThreshold: 2, requireStrategyChange: true } }, null, 2)}\n`;
151
- const contents = { queueConfig: queueContent, dispatcher: dispatcherSource(args), wrapper: wrapperSource({ queue: args.queue, loopBin }), notifier: notifierSource(args), schedulerService: schedulerServiceSource({ root: args.root, queue: args.queue }), schedulerTimer: schedulerTimerSource({ queue: args.queue }) };
165
+ const queueContent = `${JSON.stringify({ queue: args.queue, language: args.language, description: text(args.language, 'Hermes conversation queue.', 'Hermes 会话队列。'), dispatcher: 'node scripts/loops/hermes-loop-dispatch.mjs', preflightConfig: 'configs/loops/workspace-health.json', timeoutMs: 1800000, leaseMs: 1860000, staleActiveMs: 3600000, scheduler: { required: true, heartbeatMaxAgeMs: 300000, initialInterval: '1m', minInterval: '1m', maxInterval: '4h', speedupFactor: 0.5, backoffFactor: 2, idleBackoffFactor: 2, humanGateBackoffFactor: 3, longRunHeadroomFactor: 1.25, jitter: '10s' }, retry: { maxAttempts: 1, runtimeRecoveryMaxAttempts: 2, sessionMaxTicks: 10, retryDelayMs: 0, retryExitCodes: [1], requiresHumanActionPatterns: ['requires human', '需要人工', 'Permission denied', 'Operation not permitted'] }, revisionPolicy: { enabled: true, maxRevisionRounds: 3, sameFailureThreshold: 2, requireStrategyChange: true } }, null, 2)}\n`;
166
+ const contents = { queueConfig: queueContent, dispatcher: dispatcherSource(args), wrapper: wrapperSource({ queue: args.queue, loopBin, language: args.language }), notifier: notifierSource(args), schedulerService: schedulerServiceSource({ root: args.root, queue: args.queue }), schedulerTimer: schedulerTimerSource({ queue: args.queue }) };
152
167
  await mkdir(systemdUserDir, { recursive: true });
153
168
  for (const [kind, content] of Object.entries(contents)) await writeFile(files[kind], content);
154
169
  const reload = await run(args.systemctlBin, ['--user', 'daemon-reload'], { cwd: args.root }); if (reload.code !== 0) throw new Error(`Cannot reload user systemd units: ${(reload.stderr || reload.stdout).trim()}`);
155
170
  const enable = await run(args.systemctlBin, ['--user', 'enable', '--now', timer], { cwd: args.root }); if (enable.code !== 0) throw new Error(`Cannot enable Hermes Loop scheduler ${timer}: ${(enable.stderr || enable.stdout).trim()}`);
156
- const marker = instructionsBlock({ queue: args.queue }); const current = await readFile(files.instructions, 'utf8').catch(() => ''); if (!current.includes('<!-- loop-engineering:hermes:start -->')) await appendFile(files.instructions, marker);
157
- await mkdir(path.dirname(files.manifest), { recursive: true }); await writeFile(files.manifest, `${JSON.stringify({ version: 1, platform: 'hermes', installedAt: new Date().toISOString(), root: args.root, queue: args.queue, hermesBin: args.hermesBin, files: Object.entries(contents).map(([kind, content]) => ({ kind, path: files[kind], sha256: sha256(content) })), scheduler: { unit, timer } }, null, 2)}\n`);
171
+ const marker = instructionsBlock({ queue: args.queue, language: args.language }); const current = await readFile(files.instructions, 'utf8').catch(() => ''); if (!current.includes('<!-- loop-engineering:hermes:start -->')) await appendFile(files.instructions, marker);
172
+ await mkdir(path.dirname(files.manifest), { recursive: true }); await writeFile(files.manifest, `${JSON.stringify({ version: 2, platform: 'hermes', language: args.language, installedAt: new Date().toISOString(), root: args.root, queue: args.queue, hermesBin: args.hermesBin, files: Object.entries(contents).map(([kind, content]) => ({ kind, path: files[kind], sha256: sha256(content) })), scheduler: { unit, timer } }, null, 2)}\n`);
158
173
  }
159
- console.log(args.json ? JSON.stringify(report, null, 2) : `Hermes Loop installer: ${report.status}\nnext: ${args.confirmInstall ? 'Run loop-engineering-hermes-doctor, then route a harmless smoke task.' : 'Review this summary, then rerun with --confirm-install.'}`);
174
+ console.log(args.json ? JSON.stringify(report, null, 2) : text(args.language, `Hermes Loop installer: ${report.status}\nnext: ${args.confirmInstall ? 'Run loop-engineering-hermes-doctor, then route a harmless smoke task.' : 'Review this summary, then rerun with --confirm-install.'}`, `Hermes Loop 安装器:${report.status}\n下一步:${args.confirmInstall ? '运行 loop-engineering-hermes-doctor,然后路由一个无害的冒烟任务。' : '检查此摘要,然后使用 --confirm-install 重新运行。'}`));
160
175
  }
161
176
  main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; });
@@ -34,9 +34,9 @@ if (process.env.SYSTEMCTL_CAPTURE) await appendFile(process.env.SYSTEMCTL_CAPTUR
34
34
  `);
35
35
  await chmod(mockSystemctl, 0o755);
36
36
  const installer = new URL('./openclaw-install.mjs', import.meta.url).pathname;
37
- function run(args) {
37
+ function run(args, env = process.env) {
38
38
  return new Promise((resolve) => {
39
- const child = spawn(process.execPath, [installer, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
39
+ const child = spawn(process.execPath, [installer, ...args], { env, stdio: ['ignore', 'pipe', 'pipe'] });
40
40
  let stdout = ''; let stderr = '';
41
41
  child.stdout.on('data', (chunk) => { stdout += chunk; });
42
42
  child.stderr.on('data', (chunk) => { stderr += chunk; });
@@ -46,7 +46,13 @@ function run(args) {
46
46
  const installBase = ['--root', root, '--queue', 'test-tasks', '--openclaw-bin', mockOpenClaw, '--systemctl-bin', mockSystemctl];
47
47
  const plan = await run([...installBase, '--json']);
48
48
  const planReport = JSON.parse(plan.stdout);
49
- if (plan.code !== 0 || planReport.status !== 'plan_only' || planReport.platform !== 'openclaw' || planReport.workerAgent !== 'builder' || planReport.workerSelection !== 'only_available' || !planReport.workerValidated || planReport.createsWorkerAgent || planReport.confirmationSummary?.targetPlatform !== 'OpenClaw' || planReport.confirmationSummary?.writesEnabled !== false || !path.isAbsolute(planReport.confirmationSummary?.platformCli || '') || !planReport.confirmationSummary?.notificationTarget.includes('OpenClaw')) throw new Error(`plan failed: ${plan.stderr}`);
49
+ if (plan.code !== 0 || planReport.language !== 'en' || planReport.status !== 'plan_only' || planReport.platform !== 'openclaw' || planReport.workerAgent !== 'builder' || planReport.workerSelection !== 'only_available' || !planReport.workerValidated || planReport.createsWorkerAgent || planReport.confirmationSummary?.targetPlatform !== 'OpenClaw' || planReport.confirmationSummary?.writesEnabled !== false || !path.isAbsolute(planReport.confirmationSummary?.platformCli || '') || !planReport.confirmationSummary?.notificationTarget.includes('OpenClaw')) throw new Error(`plan failed: ${plan.stderr}`);
50
+ const zhPlan = await run([...installBase, '--language', 'zh']);
51
+ if (zhPlan.code !== 0 || !zhPlan.stdout.includes('安装确认') || !zhPlan.stdout.includes('目标平台:OpenClaw') || !zhPlan.stdout.includes('允许写入:否(仅生成计划)')) throw new Error('explicit Chinese installation summary missing');
52
+ const autoZhPlan = await run([...installBase, '--json'], { ...process.env, LC_ALL: 'zh_CN.UTF-8', LANG: 'C' });
53
+ if (autoZhPlan.code !== 0 || JSON.parse(autoZhPlan.stdout).language !== 'zh') throw new Error('Chinese locale was not auto-detected');
54
+ const explicitEnglishPlan = await run([...installBase, '--language', 'en', '--json'], { ...process.env, LC_ALL: 'zh_CN.UTF-8' });
55
+ if (explicitEnglishPlan.code !== 0 || JSON.parse(explicitEnglishPlan.stdout).language !== 'en') throw new Error('explicit English did not override locale');
50
56
  const humanPlan = await run(installBase);
51
57
  if (humanPlan.code !== 0 || !humanPlan.stdout.includes('Installation confirmation') || !humanPlan.stdout.includes('target platform: OpenClaw') || !humanPlan.stdout.includes('writes enabled: no (plan only)')) throw new Error('human-readable OpenClaw confirmation summary missing');
52
58
  const missingWorker = await run([...installBase, '--worker-agent', 'missing', '--json']);
@@ -55,14 +61,15 @@ const install = await run([...installBase, '--worker-agent', 'builder', '--confi
55
61
  if (install.code !== 0 || JSON.parse(install.stdout).status !== 'installed') throw new Error(`install failed: ${install.stderr}`);
56
62
  const queue = JSON.parse(await readFile(path.join(root, 'configs/loops/queues/test-tasks.json'), 'utf8'));
57
63
  if (queue.dispatcher !== 'node scripts/loops/openclaw-loop-dispatch.mjs') throw new Error('dispatcher was not installed');
64
+ if (queue.language !== 'en') throw new Error('resolved installation language was not persisted');
58
65
  if (queue.scheduler?.required !== true || queue.scheduler?.heartbeatMaxAgeMs !== 300000) throw new Error('required scheduler heartbeat was not installed');
59
66
  const dispatcher = await readFile(path.join(root, 'scripts/loops/openclaw-loop-dispatch.mjs'), 'utf8');
60
67
  if (!dispatcher.includes('already loop-managed') || !dispatcher.includes("'--agent', \"builder\"") || !dispatcher.includes('LOOP_LATEST_AMENDMENT_FILE') || !dispatcher.includes('LOOP_SESSION_GENERATION') || !dispatcher.includes('-g${sessionGeneration}')) throw new Error('worker, recursion guard, amendment polling, or session generation missing');
61
68
  if (queue.retry?.runtimeRecoveryMaxAttempts !== 2 || queue.retry?.sessionMaxTicks !== 10) throw new Error('bounded runtime recovery policy was not installed');
62
69
  const instructions = await readFile(path.join(root, 'AGENTS.md'), 'utf8');
63
- if (!instructions.includes(' loop') || !instructions.includes('immediately execute')) throw new Error('conversation instructions missing');
70
+ if (!instructions.includes('Use Loop Engineering to fix this issue') || !instructions.includes('Queue this only; do not run it yet') || /走 loop|只入队|只排队/.test(instructions)) throw new Error('English conversation instructions are missing or contain Chinese routing examples');
64
71
  const wrapper = await readFile(path.join(root, 'scripts/loops/openclaw-loop.mjs'), 'utf8');
65
- if (!wrapper.includes('--supersede-active') || !wrapper.includes('--amend-active') || !wrapper.includes('--progress-notify-command') || !wrapper.includes('runWhenUnlocked') || wrapper.includes("run-queue-drain', '--config'") || wrapper.includes("spawn('loop-engineering'") || !wrapper.includes('queue-human-input-notify') || !wrapper.includes('queue-terminal-notify') || !wrapper.includes('queue-scheduler-tick') || !wrapper.includes('只入队') || !wrapper.includes('requiredSource') || !wrapper.includes('--source-message-id')) throw new Error('supersede/amend routing, source fail-closed policy, absolute CLI, scheduler, live progress, async notification, or queue-only routing missing');
72
+ if (!wrapper.includes('--supersede-active') || !wrapper.includes('--amend-active') || !wrapper.includes('--progress-notify-command') || !wrapper.includes('runWhenUnlocked') || wrapper.includes("run-queue-drain', '--config'") || wrapper.includes("spawn('loop-engineering'") || !wrapper.includes('queue-human-input-notify') || !wrapper.includes('queue-terminal-notify') || !wrapper.includes('queue-scheduler-tick') || !wrapper.includes('queue') || !wrapper.includes('continue') || /只入队|只排队|继续当前/.test(wrapper) || !wrapper.includes('requiredSource') || !wrapper.includes('--source-message-id')) throw new Error('supersede/amend routing, source fail-closed policy, localization, absolute CLI, scheduler, live progress, async notification, or queue-only routing missing');
66
73
  const missingSourceRoute = await new Promise((resolve) => {
67
74
  const child = spawn(process.execPath, [path.join(root, 'scripts/loops/openclaw-loop.mjs'), 'route', '--message', '用 loop engineering 对齐系统'], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] });
68
75
  let stderr = ''; child.stderr.on('data', (chunk) => { stderr += chunk; });
@@ -172,4 +179,11 @@ if (await readFile(path.join(root, 'scripts/loops/openclaw-loop.mjs'), 'utf8').t
172
179
  if (await readFile(serviceFile, 'utf8').then(() => true).catch(() => false) || await readFile(timerFile, 'utf8').then(() => true).catch(() => false)) throw new Error('managed scheduler units survived uninstall');
173
180
  const finalSystemctlCalls = await readFile(systemctlCapture, 'utf8');
174
181
  if (!finalSystemctlCalls.includes('["--user","disable","--now","openclaw-loop-test-tasks-scheduler.timer"]')) throw new Error('scheduler timer was not disabled during uninstall');
182
+ const zhInstall = await run([...installBase, '--worker-agent', 'builder', '--language', 'zh', '--confirm-install', '--json']);
183
+ if (zhInstall.code !== 0 || JSON.parse(zhInstall.stdout).language !== 'zh') throw new Error(`Chinese installation failed: ${zhInstall.stderr}`);
184
+ const zhQueue = JSON.parse(await readFile(path.join(root, 'configs/loops/queues/test-tasks.json'), 'utf8'));
185
+ const zhInstructions = await readFile(path.join(root, 'AGENTS.md'), 'utf8');
186
+ const zhDispatcher = await readFile(path.join(root, 'scripts/loops/openclaw-loop-dispatch.mjs'), 'utf8');
187
+ const zhNotifier = await readFile(path.join(root, 'scripts/loops/openclaw-loop-notify.mjs'), 'utf8');
188
+ if (zhQueue.language !== 'zh' || !zhInstructions.includes('Loop Engineering 会话路由') || !zhDispatcher.includes('已经由 Loop Engineering 管理') || !zhNotifier.includes('通知器需要消息参数')) throw new Error('Chinese installation did not localize generated configuration and runtime files');
175
189
  console.log('openclaw installer self-test passed');
@@ -5,7 +5,7 @@ import { spawn } from 'node:child_process';
5
5
  import path from 'node:path';
6
6
 
7
7
  function parseArgs(argv) {
8
- const out = { root: process.cwd(), queue: 'agent-tasks', workerAgent: null, openclawBin: 'openclaw', systemctlBin: 'systemctl', json: false, confirmInstall: false, force: false };
8
+ const out = { root: process.cwd(), queue: 'agent-tasks', workerAgent: null, openclawBin: 'openclaw', systemctlBin: 'systemctl', language: 'auto', json: false, confirmInstall: false, force: false };
9
9
  for (let i = 0; i < argv.length; i++) {
10
10
  const arg = argv[i];
11
11
  if (arg === '--root') out.root = path.resolve(argv[++i]);
@@ -13,6 +13,7 @@ function parseArgs(argv) {
13
13
  else if (arg === '--worker-agent') out.workerAgent = argv[++i];
14
14
  else if (arg === '--openclaw-bin') out.openclawBin = argv[++i];
15
15
  else if (arg === '--systemctl-bin') out.systemctlBin = argv[++i];
16
+ else if (arg === '--language') out.language = argv[++i];
16
17
  else if (arg === '--confirm-install') out.confirmInstall = true;
17
18
  else if (arg === '--force') out.force = true;
18
19
  else if (arg === '--json') out.json = true;
@@ -22,6 +23,15 @@ function parseArgs(argv) {
22
23
  return out;
23
24
  }
24
25
 
26
+ function resolveLanguage(requested = 'auto', env = process.env) {
27
+ if (!['auto', 'en', 'zh'].includes(requested)) throw new Error('--language must be auto, en, or zh.');
28
+ if (requested !== 'auto') return requested;
29
+ const locale = String(env.LC_ALL || env.LC_MESSAGES || env.LANG || '').toLowerCase();
30
+ return /(^|[_.-])zh(?:[_-]|\.|$)/.test(locale) || locale.startsWith('zh') ? 'zh' : 'en';
31
+ }
32
+
33
+ const text = (language, en, zh) => language === 'zh' ? zh : en;
34
+
25
35
  function systemdEscapePath(value) {
26
36
  return [...Buffer.from(String(value))]
27
37
  .map((byte) => /[A-Za-z0-9/_.:-]/.test(String.fromCharCode(byte))
@@ -80,7 +90,17 @@ async function resolveExecutable(command) {
80
90
  throw new Error(`Executable not found on PATH: ${command}`);
81
91
  }
82
92
 
83
- function formatConfirmationSummary(summary) {
93
+ function formatConfirmationSummary(summary, language) {
94
+ if (language === 'zh') return [
95
+ '安装确认',
96
+ ` 目标平台:${summary.targetPlatform}`,
97
+ ` 平台 CLI:${summary.platformCli}`,
98
+ ` 工作区:${summary.workspace}`,
99
+ ` 队列:${summary.queue}`,
100
+ ` 调度器:${summary.scheduler}`,
101
+ ` 通知目标:${summary.notificationTarget}`,
102
+ ` 允许写入:${summary.writesEnabled ? '是' : '否(仅生成计划)'}`
103
+ ].join('\n');
84
104
  return [
85
105
  'Installation confirmation',
86
106
  ` target platform: ${summary.targetPlatform}`,
@@ -93,25 +113,43 @@ function formatConfirmationSummary(summary) {
93
113
  ].join('\n');
94
114
  }
95
115
 
96
- function dispatcherSource({ workerAgent, openclawBin }) {
116
+ function dispatcherSource({ workerAgent, openclawBin, language }) {
117
+ const promptLines = language === 'zh' ? [
118
+ '你收到的是一个已经由 Loop Engineering 管理的任务。',
119
+ '不要再次路由或入队,即使引用的用户请求中包含 loop 触发词。',
120
+ '实施前先阅读任务合同、开发计划和验收计划。'
121
+ ] : [
122
+ 'You are receiving an already loop-managed task.',
123
+ 'Do not route or enqueue this task again, even if its quoted request contains a loop trigger.',
124
+ 'Read the task contract, development plan, and acceptance plan before implementation.'
125
+ ];
126
+ const labels = language === 'zh' ? {
127
+ taskId: '任务 ID', taskContract: '任务合同', devPlan: '开发计划', acceptancePlan: '验收计划',
128
+ amendments: '实时补充要求', checkpoints: '检查点目录', missing: '未提供',
129
+ reread: '写入每个检查点以及最终完成前,如果实时补充要求文件存在,请重新读取。所有已记录的补充要求都是任务合同和验收标准的一部分。',
130
+ finish: '尽可能写入检查点,并包含最新已应用的 amendment_version。最终报告必须包含状态、证据、验证、阻塞和下一步。'
131
+ } : {
132
+ taskId: 'Task id', taskContract: 'Task contract', devPlan: 'Development plan', acceptancePlan: 'Acceptance plan',
133
+ amendments: 'Live amendments', checkpoints: 'Checkpoints dir', missing: 'not provided',
134
+ reread: 'Before writing each checkpoint and before final completion, reread the live amendment file if it exists. Treat every recorded amendment as part of the task contract and acceptance criteria.',
135
+ finish: 'Write a checkpoint when possible. Include the latest amendment_version applied. Finish with status, evidence, verification, blockers, and next action.'
136
+ };
97
137
  return `#!/usr/bin/env node
98
138
  import { readFile } from 'node:fs/promises';
99
139
  import { spawn } from 'node:child_process';
100
140
  const task = JSON.parse(await readFile(process.env.LOOP_TASK_FILE, 'utf8'));
101
141
  const sessionGeneration = Number.parseInt(process.env.LOOP_SESSION_GENERATION || '0', 10) || 0;
102
142
  const prompt = [
103
- 'You are receiving an already loop-managed task.',
104
- 'Do not route or enqueue this task again, even if its quoted request contains a loop trigger.',
105
- 'Read the task contract, development plan, and acceptance plan before implementation.',
106
- \`Task id: \${task.id}\`,
107
- \`Task contract: \${process.env.LOOP_TASK_CONTRACT_FILE || 'not provided'}\`,
108
- \`Development plan: \${process.env.LOOP_DEV_PLAN_FILE || 'not provided'}\`,
109
- \`Acceptance plan: \${process.env.LOOP_ACCEPTANCE_PLAN_FILE || 'not provided'}\`,
110
- \`Live amendments: \${process.env.LOOP_LATEST_AMENDMENT_FILE || 'not provided'}\`,
111
- \`Checkpoints dir: \${process.env.LOOP_CHECKPOINTS_DIR || 'not provided'}\`,
143
+ ${promptLines.map((line) => JSON.stringify(line)).join(',\n ')},
144
+ \`${labels.taskId}: \${task.id}\`,
145
+ \`${labels.taskContract}: \${process.env.LOOP_TASK_CONTRACT_FILE || ${JSON.stringify(labels.missing)}}\`,
146
+ \`${labels.devPlan}: \${process.env.LOOP_DEV_PLAN_FILE || ${JSON.stringify(labels.missing)}}\`,
147
+ \`${labels.acceptancePlan}: \${process.env.LOOP_ACCEPTANCE_PLAN_FILE || ${JSON.stringify(labels.missing)}}\`,
148
+ \`${labels.amendments}: \${process.env.LOOP_LATEST_AMENDMENT_FILE || ${JSON.stringify(labels.missing)}}\`,
149
+ \`${labels.checkpoints}: \${process.env.LOOP_CHECKPOINTS_DIR || ${JSON.stringify(labels.missing)}}\`,
112
150
  '', task.body, '',
113
- 'Before writing each checkpoint and before final completion, reread the live amendment file if it exists. Treat every recorded amendment as part of the task contract and acceptance criteria.',
114
- 'Write a checkpoint when possible. Include the latest amendment_version applied. Finish with status, evidence, verification, blockers, and next action.'
151
+ ${JSON.stringify(labels.reread)},
152
+ ${JSON.stringify(labels.finish)}
115
153
  ].join('\\n');
116
154
  const child = spawn(${JSON.stringify(openclawBin)}, [
117
155
  'agent', '--agent', ${JSON.stringify(workerAgent)},
@@ -122,7 +160,15 @@ child.on('close', (code, signal) => { process.exitCode = code ?? (signal ? 128 :
122
160
  `;
123
161
  }
124
162
 
125
- function wrapperSource({ queue, loopBin }) {
163
+ function wrapperSource({ queue, loopBin, language }) {
164
+ const missingSourceError = text(language, 'loop route requires conversation metadata', 'loop 路由需要会话来源元数据');
165
+ const usage = text(language, 'Usage: node scripts/loops/openclaw-loop.mjs route --message "Use loop: task" [source metadata]', '用法:node scripts/loops/openclaw-loop.mjs route --message "走 loop:任务" [来源元数据]');
166
+ const amendmentPattern = language === 'zh'
167
+ ? '(?:继续(?:当前|这个)?\\s*loop|给(?:当前|这个)?\\s*loop\\s*(?:补充|增加|加)|补充当前\\s*loop)'
168
+ : '(?:continue\\s+(?:the\\s+)?(?:current\\s+)?loop|amend\\s+(?:the\\s+)?(?:current\\s+)?loop|add\\s+(?:this\\s+)?amendment\\s+to\\s+(?:the\\s+)?(?:current\\s+)?loop)';
169
+ const queueOnlyPattern = language === 'zh'
170
+ ? '(?:只入队|只排队|暂不执行|不立即执行)'
171
+ : '(?:queue\\s+(?:this|it)\\s+only|only\\s+queue\\s+(?:this|it)|enqueue\\s+(?:this|it)\\s+only|do\\s+not\\s+(?:run|execute)\\s+(?:this|it)\\s+(?:yet|now))';
126
172
  return `#!/usr/bin/env node
127
173
  import { spawn } from 'node:child_process';
128
174
  const [command, ...rest] = process.argv.slice(2);
@@ -151,14 +197,14 @@ if (command === 'route') {
151
197
  const requiredSource = ['--source-channel', '--source-target', '--source-account', '--source-message-id'];
152
198
  const missingSource = requiredSource.filter((name) => !optionValue(name));
153
199
  if (missingSource.length) {
154
- console.error(\`loop route requires conversation metadata: \${missingSource.join(', ')}\`);
200
+ console.error(\`${missingSourceError}: \${missingSource.join(', ')}\`);
155
201
  process.exitCode = 2;
156
202
  process.exit();
157
203
  }
158
- const amendment = /(?:继续(?:当前|这个)?\\s*loop|给(?:当前|这个)?\\s*loop\\s*(?:补充|增加|加)|补充当前\\s*loop)/i.test(message);
204
+ const amendment = new RegExp(${JSON.stringify(amendmentPattern)}, 'i').test(message);
159
205
  const routeMode = amendment ? '--amend-active' : '--supersede-active';
160
206
  const routeCode = await run(['route-message', '--queue', ${JSON.stringify(queue)}, '--route', '--confirm-execute', routeMode, ...rest]);
161
- const queueOnly = /(?:只入队|只排队|暂不执行|不立即执行)/.test(message);
207
+ const queueOnly = new RegExp(${JSON.stringify(queueOnlyPattern)}, 'i').test(message);
162
208
  const runCode = routeCode || queueOnly || amendment ? routeCode : await runWhenUnlocked(['run-queue', '--config', ${JSON.stringify(`configs/loops/queues/${queue}.json`)}, '--progress-notify-command', 'node scripts/loops/openclaw-loop-notify.mjs']);
163
209
  const humanNotifyCode = await run(['queue-human-input-notify', '--queue', ${JSON.stringify(queue)}, '--notify-command', 'node scripts/loops/openclaw-loop-notify.mjs']);
164
210
  const terminalNotifyCode = await run(['queue-terminal-notify', '--queue', ${JSON.stringify(queue)}, '--notify-command', 'node scripts/loops/openclaw-loop-notify.mjs']);
@@ -174,30 +220,33 @@ if (command === 'route') {
174
220
  const terminalNotifyCode = await run(['queue-terminal-notify', '--queue', ${JSON.stringify(queue)}, '--notify-command', 'node scripts/loops/openclaw-loop-notify.mjs']);
175
221
  process.exitCode = tickCode || humanNotifyCode || terminalNotifyCode;
176
222
  } else {
177
- console.error('Usage: node scripts/loops/openclaw-loop.mjs route --message "走 loop:任务" [source metadata]');
223
+ console.error(${JSON.stringify(usage)});
178
224
  process.exitCode = 1;
179
225
  }
180
226
  `;
181
227
  }
182
228
 
183
- function schedulerServiceSource({ root, queue }) {
184
- return `[Unit]\nDescription=Taskforce Loop Engineering scheduler for ${queue}\nAfter=default.target\n\n[Service]\nType=oneshot\nWorkingDirectory=${systemdEscapePath(root)}\nExecStart=${systemdEscapePath(process.execPath)} ${systemdEscapePath(path.join(root, 'scripts', 'loops', 'openclaw-loop.mjs'))} scheduler-tick --json\n`;
229
+ function schedulerServiceSource({ root, queue, language }) {
230
+ return `[Unit]\nDescription=${text(language, `Taskforce Loop Engineering scheduler for ${queue}`, `${queue} 的 Taskforce Loop Engineering 调度器`)}\nAfter=default.target\n\n[Service]\nType=oneshot\nWorkingDirectory=${systemdEscapePath(root)}\nExecStart=${systemdEscapePath(process.execPath)} ${systemdEscapePath(path.join(root, 'scripts', 'loops', 'openclaw-loop.mjs'))} scheduler-tick --json\n`;
185
231
  }
186
232
 
187
- function schedulerTimerSource({ queue }) {
188
- return `[Unit]\nDescription=Wake Taskforce Loop Engineering scheduler for ${queue}\n\n[Timer]\nOnBootSec=30s\nOnUnitActiveSec=1min\nAccuracySec=10s\nPersistent=true\nUnit=openclaw-loop-${queue}-scheduler.service\n\n[Install]\nWantedBy=timers.target\n`;
233
+ function schedulerTimerSource({ queue, language }) {
234
+ return `[Unit]\nDescription=${text(language, `Wake Taskforce Loop Engineering scheduler for ${queue}`, `唤醒 ${queue} 的 Taskforce Loop Engineering 调度器`)}\n\n[Timer]\nOnBootSec=30s\nOnUnitActiveSec=1min\nAccuracySec=10s\nPersistent=true\nUnit=openclaw-loop-${queue}-scheduler.service\n\n[Install]\nWantedBy=timers.target\n`;
189
235
  }
190
236
 
191
- function notifierSource({ openclawBin }) {
237
+ function notifierSource({ openclawBin, language }) {
238
+ const missingMessage = text(language, 'loop notifier requires a message argument.', 'loop 通知器需要消息参数。');
239
+ const invalidSource = text(language, 'loop notifier received invalid source metadata.', 'loop 通知器收到无效的来源元数据。');
240
+ const unscoped = text(language, 'loop notifier requires source.channel and source.target; refusing an unscoped delivery.', 'loop 通知器需要 source.channel 和 source.target;拒绝无范围投递。');
192
241
  return `#!/usr/bin/env node
193
242
  import { spawn } from 'node:child_process';
194
243
  const message = process.argv.slice(2).join(' ').trim();
195
244
  const rawSource = process.env.LOOP_HUMAN_INPUT_SOURCE || process.env.LOOP_NOTIFICATION_SOURCE || '';
196
- if (!message) { console.error('loop notifier requires a message argument.'); process.exit(2); }
245
+ if (!message) { console.error(${JSON.stringify(missingMessage)}); process.exit(2); }
197
246
  let source;
198
- try { source = JSON.parse(rawSource); } catch { console.error('loop notifier received invalid source metadata.'); process.exit(2); }
247
+ try { source = JSON.parse(rawSource); } catch { console.error(${JSON.stringify(invalidSource)}); process.exit(2); }
199
248
  if (!source || typeof source !== 'object' || !source.channel || !source.target) {
200
- console.error('loop notifier requires source.channel and source.target; refusing an unscoped delivery.');
249
+ console.error(${JSON.stringify(unscoped)});
201
250
  process.exit(2);
202
251
  }
203
252
  const args = ['message', 'send', '--channel', String(source.channel), '--target', String(source.target), '--message', message, '--json'];
@@ -209,12 +258,25 @@ child.on('close', (code, signal) => { process.exitCode = code ?? (signal ? 128 :
209
258
  `;
210
259
  }
211
260
 
212
- function instructionsBlock({ queue }) {
261
+ function instructionsBlock({ queue, language }) {
262
+ if (language === 'zh') return `\n<!-- loop-engineering:openclaw:start -->
263
+ ## Loop Engineering 会话路由
264
+
265
+ - 仅路由明确要求使用 Loop 的请求。\`走 loop\` 表示入队并立即执行一次;只有 \`只入队\` 或 \`只排队\` 才不立即执行。
266
+ - \`用 loop engineering\`、\`丢进 Ironman loop\`、\`loop Ironman\` 和 \`task-runner\` 也属于明确的 Loop 请求。
267
+ - 从当前工作区运行 \`node scripts/loops/openclaw-loop.mjs route --message "<完整用户消息>"\`,并保留来源元数据。
268
+ - 会话来源任务必须使用标准包装器。缺少来源元数据时必须失败关闭,不能退回手工入队或直接运行队列。
269
+ - 人工门禁或终态只有在通知命令成功并写入通知记录后才算已送达。
270
+ - 已由 Loop 管理的任务必须直接执行,不能再次路由。
271
+ - 状态查询只读。高风险外部动作、破坏性操作、生产变更、凭据操作和记忆迁移仍需单独确认。
272
+ - 队列:\`${queue}\`。
273
+ <!-- loop-engineering:openclaw:end -->\n`;
213
274
  return `\n<!-- loop-engineering:openclaw:start -->
214
275
  ## Loop Engineering conversation routing
215
276
 
216
- - Route only explicit loop requests. \`走 loop\` means enqueue and immediately execute one tick; only \`只入队\` or \`只排队\` suppresses execution.
217
- - Treat explicit phrases such as \`用 loop engineering\`, \`丢进 Ironman loop\`, \`loop Ironman\`, and \`task-runner\` as Loop requests too.
277
+ - Route only explicit Loop Engineering requests. For example, \`Use Loop Engineering to fix this issue\` and \`Run this through Loop Engineering\` enqueue the request and immediately execute one tick.
278
+ - \`Queue this only; do not run it yet\` suppresses immediate execution. \`Continue the current loop with this amendment: ...\` amends the active task instead of replacing it.
279
+ - Treat explicit references to \`Loop Engineering\`, \`the loop\`, \`task-runner\`, or a named loop queue as Loop requests.
218
280
  - Run \`node scripts/loops/openclaw-loop.mjs route --message "<full user message>"\` from this workspace and preserve source metadata when available.
219
281
  - For conversation-originated work, the standard wrapper is mandatory. Missing source metadata must fail closed; never fall back to manual enqueue or direct run-queue.
220
282
  - A human-gated or terminal state is not delivered until its notification command succeeds and writes a notification record.
@@ -226,8 +288,9 @@ function instructionsBlock({ queue }) {
226
288
 
227
289
  async function main() {
228
290
  const args = parseArgs(process.argv.slice(2));
291
+ args.language = resolveLanguage(args.language);
229
292
  if (args.help) {
230
- console.log('Usage: loop-engineering-openclaw-install [--root workspace] [--queue agent-tasks] [--worker-agent agent-id] [--openclaw-bin openclaw] [--systemctl-bin systemctl] [--confirm-install] [--force] [--json]');
293
+ console.log('Usage: loop-engineering-openclaw-install [--root workspace] [--queue agent-tasks] [--worker-agent agent-id] [--language auto|en|zh] [--openclaw-bin openclaw] [--systemctl-bin systemctl] [--confirm-install] [--force] [--json]');
231
294
  return;
232
295
  }
233
296
  safeId(args.queue, 'queue');
@@ -252,11 +315,11 @@ async function main() {
252
315
  };
253
316
  const conflicts = [];
254
317
  for (const [kind, file] of Object.entries(files)) if (!['instructions', 'workspaceHealth', 'manifest'].includes(kind) && await exists(file)) conflicts.push(path.relative(args.root, file));
255
- const confirmationSummary = { targetPlatform: 'OpenClaw', platformCli: args.openclawBin, workspace: args.root, queue: args.queue, scheduler: `systemd user timer ${schedulerTimer}`, notificationTarget: 'source-bound at runtime (original OpenClaw conversation)', writesEnabled: args.confirmInstall };
256
- const report = { version: 1, platform: 'openclaw', status: args.confirmInstall ? 'installed' : 'plan_only', readOnly: !args.confirmInstall, root: args.root, queue: args.queue, workerAgent: args.workerAgent, workerSelection: worker.selection, availableAgents: worker.availableAgents, workerValidated: true, createsWorkerAgent: false, openclawBin: args.openclawBin, systemctlBin: args.systemctlBin, scheduler: { required: true, unit: schedulerUnit, timer: schedulerTimer }, confirmationSummary, files: Object.fromEntries(Object.entries(files).map(([key, file]) => [key, path.relative(args.root, file)])), conflicts };
257
- report.next = args.confirmInstall ? 'Run loop-engineering-openclaw-doctor, then route a harmless smoke task.' : 'Review this plan, then rerun with --confirm-install.';
318
+ const confirmationSummary = { targetPlatform: 'OpenClaw', platformCli: args.openclawBin, workspace: args.root, queue: args.queue, scheduler: `systemd user timer ${schedulerTimer}`, notificationTarget: text(args.language, 'source-bound at runtime (original OpenClaw conversation)', '运行时绑定到原始 OpenClaw 会话'), writesEnabled: args.confirmInstall };
319
+ const report = { version: 1, platform: 'openclaw', language: args.language, status: args.confirmInstall ? 'installed' : 'plan_only', readOnly: !args.confirmInstall, root: args.root, queue: args.queue, workerAgent: args.workerAgent, workerSelection: worker.selection, availableAgents: worker.availableAgents, workerValidated: true, createsWorkerAgent: false, openclawBin: args.openclawBin, systemctlBin: args.systemctlBin, scheduler: { required: true, unit: schedulerUnit, timer: schedulerTimer }, confirmationSummary, files: Object.fromEntries(Object.entries(files).map(([key, file]) => [key, path.relative(args.root, file)])), conflicts };
320
+ report.next = args.confirmInstall ? text(args.language, 'Run loop-engineering-openclaw-doctor, then route a harmless smoke task.', '运行 loop-engineering-openclaw-doctor,然后路由一个无害的冒烟任务。') : text(args.language, 'Review this plan, then rerun with --confirm-install.', '检查此计划,然后使用 --confirm-install 重新运行。');
258
321
  if (conflicts.length && !args.force && args.confirmInstall) throw new Error(`Refusing to overwrite: ${conflicts.join(', ')}. Use --force after review.`);
259
- if (!args.json) console.log(formatConfirmationSummary(confirmationSummary));
322
+ if (!args.json) console.log(formatConfirmationSummary(confirmationSummary, args.language));
260
323
  if (args.confirmInstall) {
261
324
  await mkdir(path.dirname(files.queueConfig), { recursive: true });
262
325
  await mkdir(path.dirname(files.dispatcher), { recursive: true });
@@ -271,6 +334,7 @@ async function main() {
271
334
  }
272
335
  const queueContent = `${JSON.stringify({
273
336
  queue: args.queue,
337
+ language: args.language,
274
338
  description: `OpenClaw conversation queue dispatched to agent ${args.workerAgent}.`,
275
339
  dispatcher: 'node scripts/loops/openclaw-loop-dispatch.mjs',
276
340
  preflightConfig: 'configs/loops/workspace-health.json',
@@ -300,7 +364,7 @@ async function main() {
300
364
  if (!instructions.includes('<!-- loop-engineering:openclaw:start -->')) await appendFile(files.instructions, managedInstructions);
301
365
  await mkdir(path.dirname(files.manifest), { recursive: true });
302
366
  await writeFile(files.manifest, `${JSON.stringify({
303
- version: 2, queue: args.queue, workerAgent: args.workerAgent, openclawBin: args.openclawBin, systemctlBin: args.systemctlBin, installedAt: new Date().toISOString(),
367
+ version: 3, queue: args.queue, language: args.language, workerAgent: args.workerAgent, openclawBin: args.openclawBin, systemctlBin: args.systemctlBin, installedAt: new Date().toISOString(),
304
368
  managedFiles: [
305
369
  { path: path.relative(args.root, files.queueConfig), sha256: sha256(queueContent) },
306
370
  { path: path.relative(args.root, files.dispatcher), sha256: sha256(dispatcherContent) },
@@ -315,7 +379,7 @@ async function main() {
315
379
  retainedOnUninstall: [`runtime/loops/${args.queue}`]
316
380
  }, null, 2)}\n`);
317
381
  }
318
- console.log(args.json ? JSON.stringify(report, null, 2) : `OpenClaw integration: ${report.status}\nconflicts: ${report.conflicts.join(', ') || 'none'}\nnext: ${report.next}`);
382
+ console.log(args.json ? JSON.stringify(report, null, 2) : text(args.language, `OpenClaw integration: ${report.status}\nconflicts: ${report.conflicts.join(', ') || 'none'}\nnext: ${report.next}`, `OpenClaw 集成:${report.status}\n冲突:${report.conflicts.join(', ') || '无'}\n下一步:${report.next}`));
319
383
  }
320
384
 
321
385
  main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; });
@@ -73,7 +73,7 @@ async function main() {
73
73
  if (!plan.ready) throw new Error(`Refusing upgrade because managed content changed: ${[...modified, ...(!instructionsClean ? ['AGENTS.md managed block'] : [])].join(', ')}`);
74
74
  const installer = new URL('./openclaw-install.mjs', import.meta.url).pathname;
75
75
  const result = await new Promise((resolve) => {
76
- const child = spawn(process.execPath, [installer, '--root', args.root, '--queue', manifest.queue, '--worker-agent', manifest.workerAgent, '--openclaw-bin', manifest.openclawBin || 'openclaw', '--systemctl-bin', manifest.systemctlBin || 'systemctl', '--confirm-install', '--force', '--json'], { stdio: ['ignore', 'pipe', 'pipe'] });
76
+ const child = spawn(process.execPath, [installer, '--root', args.root, '--queue', manifest.queue, '--worker-agent', manifest.workerAgent, '--language', manifest.language || 'en', '--openclaw-bin', manifest.openclawBin || 'openclaw', '--systemctl-bin', manifest.systemctlBin || 'systemctl', '--confirm-install', '--force', '--json'], { stdio: ['ignore', 'pipe', 'pipe'] });
77
77
  let stdout = ''; let stderr = ''; child.stdout.on('data', (c) => { stdout += c; }); child.stderr.on('data', (c) => { stderr += c; });
78
78
  child.on('close', (code) => resolve({ code, stdout, stderr }));
79
79
  });
@@ -54,6 +54,9 @@ assert.deepEqual(classifyLoopMessage('用 loop engineering 把现有的 growth o
54
54
  enqueue: true,
55
55
  readOnly: false
56
56
  });
57
+ assert.equal(classifyLoopMessage('Use Loop Engineering to fix this issue.').intent, 'execute');
58
+ assert.equal(classifyLoopMessage('Run this through Loop Engineering.').intent, 'execute');
59
+ assert.equal(classifyLoopMessage('Continue the current loop with this amendment: add English examples.').intent, 'execute');
57
60
 
58
61
  const routed = await routeLoopMessage(root, {
59
62
  route: true,
@@ -380,10 +383,12 @@ await writeJson(checkpointFile, {
380
383
  const failedFile = path.join(queueSubdirFor(root, queue, 'failed'), path.basename(terminalFile));
381
384
  await writeJson(failedFile, terminalTask);
382
385
  if (failedFile !== terminalFile) await rm(terminalFile, { force: true });
386
+ await writeJson(path.join(root, 'configs', 'loops', 'queues', `${queue}.json`), { queue, language: 'zh' });
383
387
 
384
388
  const gateDryRun = await notifyHumanInputRequests(root, { queue, dryRun: true });
385
389
  assert.equal(gateDryRun.results[0].outcome, 'dry_run');
386
390
  assert.match(gateDryRun.results[0].message, /Provide the SMS code/);
391
+ assert.match(gateDryRun.results[0].message, /正在等待你的输入/);
387
392
  const gateSent = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
388
393
  assert.equal(gateSent.sent, 1);
389
394
  const gateId = gateSent.results[0].gateId;
@@ -470,6 +475,7 @@ await writeJson(failedFile, { ...requeuedTask, status: 'needs_human_input' });
470
475
  await rm(inboxRequeued, { force: true });
471
476
 
472
477
  const dryRun = await notifyTerminalTasks(root, { queue, dryRun: true });
478
+ assert.match(dryRun.results[0].message, /Loop 任务/);
473
479
  assert.equal(dryRun.results[0].outcome, 'dry_run');
474
480
  const sent = await notifyTerminalTasks(root, { queue, notifyCommand: '/bin/true' });
475
481
  assert.equal(sent.sent, 1);