harnessed 3.3.1 → 3.4.1

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/dist/cli.mjs CHANGED
@@ -11,12 +11,12 @@ import lockfile from 'proper-lockfile';
11
11
  import { Command } from 'commander';
12
12
  import { Ajv } from 'ajv';
13
13
  import * as ajvFormatsNs from 'ajv-formats';
14
+ import { fileURLToPath } from 'url';
14
15
  import { createHash } from 'crypto';
15
16
  import { query } from '@anthropic-ai/claude-agent-sdk';
16
17
  import * as p from '@clack/prompts';
17
18
  import { createPatch } from 'diff';
18
19
  import pc from 'picocolors';
19
- import { fileURLToPath } from 'url';
20
20
  import { stdout, stdin } from 'process';
21
21
  import * as readline from 'readline/promises';
22
22
 
@@ -498,7 +498,7 @@ function checkTokenBudget() {
498
498
  const msg = `${items.length} skill(s) total ${total} tokens (under 1% / 2000 threshold)`;
499
499
  return { name: "token budget", status: "pass", message: msg };
500
500
  }
501
- const top = [...items].sort((a, b) => b.tokens - a.tokens).slice(0, 3).map((t) => `${t.name}:${t.tokens}`).join(", ");
501
+ const top = [...items].sort((a, b) => b.tokens - a.tokens).slice(0, 3).map((t2) => `${t2.name}:${t2.tokens}`).join(", ");
502
502
  const pct = (total / CONTEXT_WINDOW_TOKENS * 100).toFixed(2);
503
503
  const message = `${items.length} skill(s) total ${total} tokens (${pct}% of 200000) \u2014 top: ${top}`;
504
504
  return {
@@ -847,7 +847,7 @@ var init_resume = __esm({
847
847
 
848
848
  // package.json
849
849
  var package_default = {
850
- version: "3.3.1"};
850
+ version: "3.4.1"};
851
851
 
852
852
  // src/manifest/errors.ts
853
853
  function instancePathToKeyPath(instancePath) {
@@ -1600,6 +1600,71 @@ audited ${yamls.length} manifest${yamls.length === 1 ? "" : "s"} \u2014 ${findin
1600
1600
  process.exit(errorCount > 0 ? 1 : 0);
1601
1601
  });
1602
1602
  }
1603
+ var SUPPORTED = /* @__PURE__ */ new Set(["en", "zh-Hans"]);
1604
+ var currentLocale = null;
1605
+ var cache = {};
1606
+ function mapToSupported(raw) {
1607
+ if (!raw) return "en";
1608
+ if (/^zh([^a-z]|$)/i.test(raw)) return "zh-Hans";
1609
+ return "en";
1610
+ }
1611
+ function detectLocale() {
1612
+ const raw = process.env.HARNESSED_LANG || process.env.LC_ALL || process.env.LANG || process.env.LANGUAGE || safeIntlLocale();
1613
+ return mapToSupported(raw);
1614
+ }
1615
+ function safeIntlLocale() {
1616
+ try {
1617
+ return Intl.DateTimeFormat().resolvedOptions().locale;
1618
+ } catch {
1619
+ return void 0;
1620
+ }
1621
+ }
1622
+ function messagesDir() {
1623
+ const here = dirname(fileURLToPath(import.meta.url));
1624
+ const candidates = [resolve(here, "..", "..", "messages"), resolve(here, "..", "messages")];
1625
+ for (const c of candidates) {
1626
+ try {
1627
+ readFileSync(join(c, "en.json"), "utf8");
1628
+ return c;
1629
+ } catch {
1630
+ }
1631
+ }
1632
+ return resolve(process.cwd(), "messages");
1633
+ }
1634
+ function loadLocale(locale) {
1635
+ if (cache[locale]) return cache[locale];
1636
+ const path = join(messagesDir(), `${locale}.json`);
1637
+ try {
1638
+ const raw = readFileSync(path, "utf8");
1639
+ cache[locale] = JSON.parse(raw);
1640
+ } catch {
1641
+ cache[locale] = {};
1642
+ }
1643
+ return cache[locale];
1644
+ }
1645
+ function setLocale(locale) {
1646
+ if (!locale) return;
1647
+ const mapped = mapToSupported(locale);
1648
+ if (SUPPORTED.has(mapped)) currentLocale = mapped;
1649
+ }
1650
+ function getLocale() {
1651
+ if (currentLocale === null) currentLocale = detectLocale();
1652
+ return currentLocale;
1653
+ }
1654
+ function t(key, params) {
1655
+ const locale = getLocale();
1656
+ const primary = loadLocale(locale);
1657
+ let template = primary[key];
1658
+ if (template === void 0 && locale !== "en") {
1659
+ template = loadLocale("en")[key];
1660
+ }
1661
+ if (template === void 0) template = key;
1662
+ if (!params) return template;
1663
+ return template.replace(/\{\{(\w+)\}\}/g, (_match, name) => {
1664
+ const v = params[name];
1665
+ return v === void 0 ? `{{${name}}}` : String(v);
1666
+ });
1667
+ }
1603
1668
 
1604
1669
  // src/cli/audit-log.ts
1605
1670
  init_harnessedRoot();
@@ -1633,7 +1698,7 @@ function renderHumanTable(records) {
1633
1698
  }
1634
1699
  }
1635
1700
  function pipeToJq(filterExpr, lines) {
1636
- return new Promise((resolve10, reject) => {
1701
+ return new Promise((resolve12, reject) => {
1637
1702
  const child = spawn("jq", [filterExpr], {
1638
1703
  stdio: ["pipe", "inherit", "inherit"],
1639
1704
  windowsHide: true
@@ -1641,13 +1706,13 @@ function pipeToJq(filterExpr, lines) {
1641
1706
  child.on("error", (err2) => {
1642
1707
  const e = err2;
1643
1708
  if (e.code === "ENOENT") {
1644
- console.error("\u2717 jq not found in PATH \u2014 run: harnessed doctor");
1645
- resolve10(1);
1709
+ console.error(t("audit_log.jq_missing"));
1710
+ resolve12(1);
1646
1711
  } else {
1647
1712
  reject(err2);
1648
1713
  }
1649
1714
  });
1650
- child.on("close", (code) => resolve10(code ?? 0));
1715
+ child.on("close", (code) => resolve12(code ?? 0));
1651
1716
  child.stdin.write(lines.join("\n"));
1652
1717
  child.stdin.end();
1653
1718
  });
@@ -1659,23 +1724,23 @@ function registerAuditLog(program2) {
1659
1724
  async (opts) => {
1660
1725
  const tailN = opts.tail !== void 0 ? Number(opts.tail) : 50;
1661
1726
  if (Number.isNaN(tailN) || tailN < 1) {
1662
- console.error("\u2717 --tail must be a positive integer");
1727
+ console.error(t("audit_log.tail_invalid"));
1663
1728
  process.exit(2);
1664
1729
  }
1665
1730
  const headN = opts.head !== void 0 ? Number(opts.head) : void 0;
1666
1731
  if (headN !== void 0 && (Number.isNaN(headN) || headN < 1)) {
1667
- console.error("\u2717 --head must be a positive integer");
1732
+ console.error(t("audit_log.head_invalid"));
1668
1733
  process.exit(2);
1669
1734
  }
1670
1735
  const path = auditPath();
1671
1736
  if (!existsSync(path)) {
1672
- console.log(`no audit records found (${path} does not exist)`);
1737
+ console.log(t("audit_log.no_records_file", { path }));
1673
1738
  process.exit(0);
1674
1739
  }
1675
1740
  const raw = readFileSync(path, "utf8");
1676
1741
  const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
1677
1742
  if (lines.length === 0) {
1678
- console.log("no audit records found (audit.log is empty)");
1743
+ console.log(t("audit_log.no_records_empty"));
1679
1744
  process.exit(0);
1680
1745
  }
1681
1746
  let records = [];
@@ -1832,11 +1897,11 @@ function registerBackupList(program2) {
1832
1897
  try {
1833
1898
  dirs = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
1834
1899
  } catch {
1835
- console.log(`no backups found (${root} absent)`);
1900
+ console.log(t("backup.no_backups", { root }));
1836
1901
  return;
1837
1902
  }
1838
1903
  if (dirs.length === 0) {
1839
- console.log(`no backups found (${root} empty)`);
1904
+ console.log(t("backup.no_backups_empty", { root }));
1840
1905
  return;
1841
1906
  }
1842
1907
  for (const ts of dirs) {
@@ -1851,8 +1916,7 @@ function registerBackupList(program2) {
1851
1916
  console.log(`${ts} (metadata.json missing or unreadable)`);
1852
1917
  }
1853
1918
  }
1854
- console.log(`
1855
- ${dirs.length} snapshot${dirs.length === 1 ? "" : "s"} total`);
1919
+ console.log(t("backup.total_snapshots", { count: dirs.length }));
1856
1920
  });
1857
1921
  }
1858
1922
  function checkNodeVersion() {
@@ -1998,7 +2062,7 @@ function registerDoctor(program2) {
1998
2062
  if (r.status !== "pass" && r.fix) console.log(` fix: ${r.fix}`);
1999
2063
  }
2000
2064
  console.log(
2001
- hasFail ? "\nsome checks failed (see fix hints above)" : hasWarn ? "\nall checks ok (with warnings \u2014 see hints above)" : "\nall checks passed"
2065
+ hasFail ? t("doctor.summary.fail") : hasWarn ? t("doctor.summary.warn") : t("doctor.summary.pass")
2002
2066
  );
2003
2067
  }
2004
2068
  process.exit(hasFail ? 1 : 0);
@@ -3005,7 +3069,7 @@ function registerExecuteTask(program2) {
3005
3069
  "Run execute-task workflow (4-phase chain \u2192 ralph-loop COMPLETE; immediate by default \u2014 use --dry-run for preview)"
3006
3070
  ).requiredOption("--task <text>", "task description (required)").option("--workflow <name>", "workflow name", "execute-task").option("--dry-run", "preview only \u2014 do not spawn subagent (opt-in for advanced users)").option("--non-interactive", "CI / scripts").option("--model <model>", "subagent model: 'haiku' | 'sonnet' | 'opus'").option("--model-tier <tier>", "override: 'inherit' bypasses per-phase phase.model (B-10)").option("--max-iterations <n>", "ralph-loop max iter (default 20)", (v) => parseInt(v, 10)).action(async (raw) => {
3007
3071
  if (!raw.task) {
3008
- console.error("error: --task <text> is required");
3072
+ console.error(t("execute_task.require_task"));
3009
3073
  process.exit(2);
3010
3074
  }
3011
3075
  const workflowName = raw.workflow ?? "execute-task";
@@ -3014,7 +3078,10 @@ function registerExecuteTask(program2) {
3014
3078
  phases = loadPhases(`workflows/${workflowName}/phases.yaml`);
3015
3079
  } catch (error) {
3016
3080
  console.error(
3017
- `error: failed to load workflows/${workflowName}/phases.yaml \u2014 ${error.message}`
3081
+ t("execute_task.load_phases_failed", {
3082
+ workflow: workflowName,
3083
+ message: error.message
3084
+ })
3018
3085
  );
3019
3086
  process.exit(2);
3020
3087
  }
@@ -3053,9 +3120,7 @@ function registerExecuteTask(program2) {
3053
3120
  // apply-immediate default
3054
3121
  });
3055
3122
  } catch (err2) {
3056
- console.warn(
3057
- `\u26A0\uFE0F before-commit pre-flight skipped (${err2.message}); subagent will biome-check at commit time.`
3058
- );
3123
+ console.warn(t("execute_task.precommit_skipped", { message: err2.message }));
3059
3124
  }
3060
3125
  const result = await runRouting(taskCtx, {
3061
3126
  maxIterations: raw.maxIterations ?? 20,
@@ -3064,7 +3129,7 @@ function registerExecuteTask(program2) {
3064
3129
  ...fallbackPhaseId ? { fallbackPhaseId } : {}
3065
3130
  });
3066
3131
  if ("aborted" in result) {
3067
- console.error(`aborted: ${result.reason}`);
3132
+ console.error(t("install.aborted", { reason: result.reason }));
3068
3133
  process.exit(2);
3069
3134
  }
3070
3135
  if ("ok" in result && result.ok === false) {
@@ -3110,8 +3175,8 @@ function registerGc(program2) {
3110
3175
  const olderMs = parseDuration(opts.olderThan ?? "30d");
3111
3176
  if (olderMs == null) {
3112
3177
  console.error(
3113
- `error: invalid --older-than '${opts.olderThan}'
3114
- fix: use format <N>{d|h|m|w} e.g. 30d / 24h / 60m / 4w`
3178
+ `${t("gc.invalid_duration", { value: opts.olderThan ?? "" })}
3179
+ ${t("gc.invalid_duration.fix")}`
3115
3180
  );
3116
3181
  process.exit(1);
3117
3182
  return;
@@ -3122,7 +3187,7 @@ function registerGc(program2) {
3122
3187
  try {
3123
3188
  dirs = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
3124
3189
  } catch {
3125
- console.log(`no backups found (${root} absent) \u2014 nothing to gc`);
3190
+ console.log(t("gc.no_backups", { root }));
3126
3191
  return;
3127
3192
  }
3128
3193
  const cutoff = Date.now() - olderMs;
@@ -3132,8 +3197,8 @@ function registerGc(program2) {
3132
3197
  if (kept.has(ts)) continue;
3133
3198
  const path = join(root, ts);
3134
3199
  const iso = ts.replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3");
3135
- const t = Date.parse(iso);
3136
- if (Number.isNaN(t) || t > cutoff) continue;
3200
+ const t2 = Date.parse(iso);
3201
+ if (Number.isNaN(t2) || t2 > cutoff) continue;
3137
3202
  let manifest = "(unknown)";
3138
3203
  try {
3139
3204
  const meta = JSON.parse(
@@ -3146,17 +3211,19 @@ function registerGc(program2) {
3146
3211
  candidates.push({ ts, path, manifest, sizeKb });
3147
3212
  }
3148
3213
  if (candidates.length === 0) {
3149
- console.log(`no snapshots older than ${opts.olderThan} (kept ${kept.size} most-recent)`);
3214
+ console.log(
3215
+ t("gc.no_old_snapshots", { cutoff: opts.olderThan ?? "", keptCount: kept.size })
3216
+ );
3150
3217
  return;
3151
3218
  }
3152
3219
  const totalKb = candidates.reduce((a, c) => a + c.sizeKb, 0);
3153
- const verb = dryRun ? "would delete" : "deleting";
3154
- console.log(`${verb} ${candidates.length} snapshot(s), ~${totalKb} KB total:`);
3220
+ const key = dryRun ? "gc.summary_will_delete" : "gc.summary_deleting";
3221
+ console.log(t(key, { count: candidates.length, sizeKb: totalKb }));
3155
3222
  for (const c of candidates) {
3156
3223
  console.log(` ${c.ts} ${c.manifest} (${c.sizeKb} KB)`);
3157
3224
  if (!dryRun) await rm(c.path, { recursive: true, force: true });
3158
3225
  }
3159
- if (dryRun) console.log("\n(dry-run \u2014 re-run without --dry-run to actually delete)");
3226
+ if (dryRun) console.log(t("gc.dry_run_rerun_hint"));
3160
3227
  });
3161
3228
  }
3162
3229
  async function confirmAt(level, ctx) {
@@ -3364,10 +3431,10 @@ var installCcHookAdd = async (ctx) => {
3364
3431
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed", "preflight");
3365
3432
  return { ok: false, phase: "preflight", error: e };
3366
3433
  }
3367
- const settingsPath2 = join(homedir(), ".claude", "settings.json");
3434
+ const settingsPath3 = join(homedir(), ".claude", "settings.json");
3368
3435
  let existing;
3369
3436
  try {
3370
- existing = await readFile(settingsPath2, "utf8");
3437
+ existing = await readFile(settingsPath3, "utf8");
3371
3438
  } catch {
3372
3439
  existing = null;
3373
3440
  }
@@ -3398,7 +3465,7 @@ var installCcHookAdd = async (ctx) => {
3398
3465
  const newText = `${JSON.stringify(settings, null, 2)}
3399
3466
  `;
3400
3467
  const plan = {
3401
- files: [{ target: settingsPath2, scope: "HOME", oldText: existing ?? "", newText }]
3468
+ files: [{ target: settingsPath3, scope: "HOME", oldText: existing ?? "", newText }]
3402
3469
  };
3403
3470
  process.stdout.write(renderDiff(plan, ctx));
3404
3471
  const conf = await confirmAt("L3", { ...ctx});
@@ -3409,10 +3476,10 @@ var installCcHookAdd = async (ctx) => {
3409
3476
  if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
3410
3477
  const bk = await backup(plan, ctx);
3411
3478
  if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
3412
- await writeFile(settingsPath2, newText);
3479
+ await writeFile(settingsPath3, newText);
3413
3480
  let verify;
3414
3481
  try {
3415
- verify = JSON.parse(await readFile(settingsPath2, "utf8"));
3482
+ verify = JSON.parse(await readFile(settingsPath3, "utf8"));
3416
3483
  } catch (e) {
3417
3484
  return {
3418
3485
  ok: false,
@@ -3440,7 +3507,7 @@ var installCcHookAdd = async (ctx) => {
3440
3507
  };
3441
3508
  }
3442
3509
  await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, "", "");
3443
- return { ok: true, backupId: bk.backupId, appliedFiles: [settingsPath2] };
3510
+ return { ok: true, backupId: bk.backupId, appliedFiles: [settingsPath3] };
3444
3511
  };
3445
3512
  function getUserClaudeJsonPath() {
3446
3513
  return join(homedir(), ".claude.json");
@@ -3476,7 +3543,7 @@ async function isPluginRegistered(pluginName) {
3476
3543
  return Object.keys(plugins).some((k) => k.split("@")[0] === pluginName);
3477
3544
  }
3478
3545
  function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
3479
- return new Promise((resolve10) => {
3546
+ return new Promise((resolve12) => {
3480
3547
  const isWin = process.platform === "win32";
3481
3548
  const child = isWin ? spawn("cmd.exe", ["/c", "claude", ...claudeArgs], { cwd, windowsHide: true }) : spawn("claude", claudeArgs, { cwd, shell: false });
3482
3549
  let stderr = "";
@@ -3485,15 +3552,15 @@ function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
3485
3552
  });
3486
3553
  const timer = setTimeout(() => {
3487
3554
  child.kill("SIGKILL");
3488
- resolve10({ exitCode: -1, stderr: `${stderr}[timeout after ${timeoutMs}ms]` });
3555
+ resolve12({ exitCode: -1, stderr: `${stderr}[timeout after ${timeoutMs}ms]` });
3489
3556
  }, timeoutMs);
3490
3557
  child.on("error", (e) => {
3491
3558
  clearTimeout(timer);
3492
- resolve10({ exitCode: -1, stderr: `${stderr}${e.message}` });
3559
+ resolve12({ exitCode: -1, stderr: `${stderr}${e.message}` });
3493
3560
  });
3494
3561
  child.on("close", (code) => {
3495
3562
  clearTimeout(timer);
3496
- resolve10({ exitCode: code ?? -1, stderr });
3563
+ resolve12({ exitCode: code ?? -1, stderr });
3497
3564
  });
3498
3565
  });
3499
3566
  }
@@ -3672,10 +3739,10 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
3672
3739
  child.stderr?.setEncoding("utf8").on("data", (chunk) => {
3673
3740
  stderr += chunk;
3674
3741
  });
3675
- return await new Promise((resolve10) => {
3742
+ return await new Promise((resolve12) => {
3676
3743
  const timer = setTimeout(() => {
3677
3744
  child.kill("SIGKILL");
3678
- resolve10({
3745
+ resolve12({
3679
3746
  ok: false,
3680
3747
  phase: "spawn",
3681
3748
  error: {
@@ -3690,7 +3757,7 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
3690
3757
  }, effectiveTimeoutMs);
3691
3758
  child.on("error", (err2) => {
3692
3759
  clearTimeout(timer);
3693
- resolve10({
3760
+ resolve12({
3694
3761
  ok: false,
3695
3762
  phase: "spawn",
3696
3763
  error: {
@@ -3705,14 +3772,14 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
3705
3772
  });
3706
3773
  child.on("close", (code) => {
3707
3774
  clearTimeout(timer);
3708
- resolve10({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
3775
+ resolve12({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
3709
3776
  });
3710
3777
  });
3711
3778
  }
3712
3779
 
3713
3780
  // src/installers/gitCloneWithSetup.ts
3714
3781
  function gitRevParseHead(cwd, timeoutMs = 1e4) {
3715
- return new Promise((resolve10) => {
3782
+ return new Promise((resolve12) => {
3716
3783
  const isWin = process.platform === "win32";
3717
3784
  const child = isWin ? spawn("cmd.exe", ["/c", "git", "rev-parse", "HEAD"], { cwd, windowsHide: true }) : spawn("git", ["rev-parse", "HEAD"], { cwd, shell: false });
3718
3785
  let stdout2 = "";
@@ -3721,15 +3788,15 @@ function gitRevParseHead(cwd, timeoutMs = 1e4) {
3721
3788
  });
3722
3789
  const timer = setTimeout(() => {
3723
3790
  child.kill("SIGKILL");
3724
- resolve10({ sha: "", exit: -1 });
3791
+ resolve12({ sha: "", exit: -1 });
3725
3792
  }, timeoutMs);
3726
3793
  child.on("error", () => {
3727
3794
  clearTimeout(timer);
3728
- resolve10({ sha: "", exit: -1 });
3795
+ resolve12({ sha: "", exit: -1 });
3729
3796
  });
3730
3797
  child.on("close", (code) => {
3731
3798
  clearTimeout(timer);
3732
- resolve10({ sha: stdout2.trim(), exit: code ?? -1 });
3799
+ resolve12({ sha: stdout2.trim(), exit: code ?? -1 });
3733
3800
  });
3734
3801
  });
3735
3802
  }
@@ -3740,11 +3807,11 @@ function extractCloneTarget(cmd) {
3740
3807
  const tokens = tail.split(/\s+/);
3741
3808
  let i = 0;
3742
3809
  while (i < tokens.length) {
3743
- const t = tokens[i];
3744
- if (t === void 0 || !t.startsWith("-")) break;
3745
- if (t === "--depth" || t === "--branch" || t === "-b") {
3810
+ const t2 = tokens[i];
3811
+ if (t2 === void 0 || !t2.startsWith("-")) break;
3812
+ if (t2 === "--depth" || t2 === "--branch" || t2 === "-b") {
3746
3813
  i += 2;
3747
- } else if (t.includes("=")) {
3814
+ } else if (t2.includes("=")) {
3748
3815
  i += 1;
3749
3816
  } else {
3750
3817
  i += 2;
@@ -4479,8 +4546,8 @@ function registerInstall(program2) {
4479
4546
  chosenPath = skillPackPath;
4480
4547
  } catch {
4481
4548
  console.error(
4482
- `error: manifest '${resolvedName}' not found
4483
- fix: ensure manifests/tools/${resolvedName}.yaml or manifests/skill-packs/${resolvedName}.yaml exists`
4549
+ `${t("install.manifest_not_found", { name: resolvedName })}
4550
+ ${t("install.manifest_not_found.fix", { name: resolvedName })}`
4484
4551
  );
4485
4552
  process.exit(1);
4486
4553
  }
@@ -4488,7 +4555,7 @@ function registerInstall(program2) {
4488
4555
  const v = validateManifestFile(yamlSrc, chosenPath);
4489
4556
  if (!v.ok) {
4490
4557
  for (const e of v.errors) console.error(`error: ${e.message} at ${e.path}`);
4491
- console.error(` fix: run 'harnessed audit' to inspect manifest issues`);
4558
+ console.error(t("install.audit_hint"));
4492
4559
  process.exit(1);
4493
4560
  }
4494
4561
  const dryRun = raw.dryRun === true;
@@ -4510,12 +4577,14 @@ function registerInstall(program2) {
4510
4577
  }
4511
4578
  const result = await runInstall(v.manifest, opts);
4512
4579
  if ("aborted" in result) {
4513
- console.error(`aborted: ${result.reason}`);
4580
+ console.error(t("install.aborted", { reason: result.reason }));
4514
4581
  process.exit(2);
4515
4582
  }
4516
4583
  if (result.ok) {
4517
4584
  const version = v.manifest.spec.install.method === "npm-cli" && "npm_version" in v.manifest.spec.install ? v.manifest.spec.install.npm_version : "";
4518
- console.log(`installed ${v.manifest.metadata.name}${version ? `@${version}` : ""}`);
4585
+ console.log(
4586
+ version ? t("install.success_with_version", { name: v.manifest.metadata.name, version }) : t("install.success", { name: v.manifest.metadata.name })
4587
+ );
4519
4588
  process.exit(0);
4520
4589
  }
4521
4590
  console.error(formatError(result.error));
@@ -4613,10 +4682,8 @@ function registerManifestAdd(program2) {
4613
4682
  const category = raw.category ?? "skill-packs";
4614
4683
  const outPath = `manifests/${category}/${name}.ee5-answers.json`;
4615
4684
  if (raw.nonInteractive) {
4616
- console.warn(
4617
- "[ee-5-gate] WARN: --non-interactive skips 5-question prompt (D-03 dry-run-only). plan-phase hard reject still applies."
4618
- );
4619
- console.log(`[manifest-add] dry-run preview for upstream: ${upstream} \u2192 ${outPath}`);
4685
+ console.warn(t("manifest_add.non_interactive_warn"));
4686
+ console.log(t("manifest_add.dry_run_preview", { upstream, path: outPath }));
4620
4687
  process.exit(0);
4621
4688
  }
4622
4689
  const rl = readline.createInterface({ input: stdin, output: stdout });
@@ -4629,7 +4696,7 @@ function registerManifestAdd(program2) {
4629
4696
  const a = (await rl.question(`${q}
4630
4697
  > `)).trim();
4631
4698
  if (!a) {
4632
- console.error("error: EE-5 gate requires non-empty answer");
4699
+ console.error(t("manifest_add.empty_answer"));
4633
4700
  rl.close();
4634
4701
  process.exit(1);
4635
4702
  }
@@ -4640,9 +4707,9 @@ function registerManifestAdd(program2) {
4640
4707
  if (!dryRun) {
4641
4708
  writeFileSync(outPath, `${JSON.stringify(payload, null, 2)}
4642
4709
  `, "utf8");
4643
- console.log(`[manifest-add] EE-5 gate passed; wrote ${outPath}`);
4710
+ console.log(t("manifest_add.gate_passed_wrote", { path: outPath }));
4644
4711
  } else {
4645
- console.log(`[manifest-add] EE-5 gate passed (dry-run); would write ${outPath}`);
4712
+ console.log(t("manifest_add.gate_passed_dry_run", { path: outPath }));
4646
4713
  console.log(JSON.stringify(payload, null, 2));
4647
4714
  }
4648
4715
  process.exit(0);
@@ -4655,7 +4722,7 @@ function registerResearch(program2) {
4655
4722
  "Run research workflow (search category sub-routing \u2192 spawn \u2192 verbatim COMPLETE; immediate by default \u2014 use --dry-run for preview)"
4656
4723
  ).requiredOption("--query <text>", "research prompt (required)").option("--dry-run", "preview only \u2014 do not spawn subagent (opt-in for advanced users)").option("--non-interactive", "skip all prompts (CI / scripts)").option("--model <model>", "subagent model: 'haiku' | 'sonnet' | 'opus'").action(async (raw) => {
4657
4724
  if (!raw.query) {
4658
- console.error("error: --query <text> is required");
4725
+ console.error(t("research.require_query"));
4659
4726
  process.exit(2);
4660
4727
  }
4661
4728
  const taskCtx = { task: raw.query, task_type: "search" };
@@ -4668,31 +4735,33 @@ function registerResearch(program2) {
4668
4735
  ...raw.model ? { agentOpts: { modelOverride: raw.model } } : {}
4669
4736
  });
4670
4737
  if ("aborted" in preview) {
4671
- console.error(`aborted: ${preview.reason}`);
4738
+ console.error(t("install.aborted", { reason: preview.reason }));
4672
4739
  process.exit(2);
4673
4740
  }
4674
4741
  if ("ok" in preview && preview.ok === false) {
4675
4742
  console.error(`error: ${preview.phase} \u2014 ${preview.error.message}`);
4676
4743
  process.exit(1);
4677
4744
  }
4678
- console.log(`[dry-run] matched_rule: ${preview.matchedRule?.id ?? "(fallback supervisor)"}`);
4679
- console.log(`[dry-run] query: ${raw.query}`);
4680
4745
  console.log(
4681
- " (run without --dry-run to spawn the subagent and emit verbatim COMPLETE round-trip)"
4746
+ t("research.dry_run.matched_rule", {
4747
+ rule: preview.matchedRule?.id ?? "(fallback supervisor)"
4748
+ })
4682
4749
  );
4750
+ console.log(t("research.dry_run.query", { query: raw.query }));
4751
+ console.log(t("research.dry_run.run_hint"));
4683
4752
  process.exit(0);
4684
4753
  }
4685
4754
  const result = await runRouting(taskCtx, {
4686
4755
  ...raw.model ? { agentOpts: { modelOverride: raw.model } } : {}
4687
4756
  });
4688
4757
  if ("aborted" in result) {
4689
- console.error(`aborted: ${result.reason}`);
4758
+ console.error(t("install.aborted", { reason: result.reason }));
4690
4759
  process.exit(2);
4691
4760
  }
4692
4761
  if ("ok" in result && result.ok === false) {
4693
4762
  console.error(`error: ${result.phase} \u2014 ${result.error.message}`);
4694
4763
  if (result.phase === "install") {
4695
- console.error(` fix: 'harnessed install <skill>' (see error above)`);
4764
+ console.error(t("research.install_fix_hint"));
4696
4765
  }
4697
4766
  process.exit(1);
4698
4767
  }
@@ -4714,12 +4783,11 @@ function registerResume(program2) {
4714
4783
  return;
4715
4784
  }
4716
4785
  if (r.status === "no-paused-phase") {
4717
- console.error(`\u2717 ${r.error}`);
4786
+ console.error(t("resume.fail", { error: r.error }));
4718
4787
  process.exit(1);
4719
4788
  }
4720
4789
  if (r.status === "corrupt") {
4721
- console.error(`\u2717 ${r.error}
4722
- path: ${r.path}`);
4790
+ console.error(t("resume.corrupt", { error: r.error, path: r.path }));
4723
4791
  process.exit(1);
4724
4792
  }
4725
4793
  if (r.cwdWarn) console.error(r.cwdWarn);
@@ -4748,8 +4816,8 @@ function registerRollback(program2) {
4748
4816
  meta = JSON.parse(await readFile(metaPath, "utf8"));
4749
4817
  } catch (err2) {
4750
4818
  console.error(
4751
- `error: cannot read ${metaPath}: ${err2.message}
4752
- fix: run 'harnessed backup list' to see available timestamps`
4819
+ `${t("rollback.metadata_unreadable", { path: metaPath, message: err2.message })}
4820
+ ${t("rollback.metadata_unreadable.fix")}`
4753
4821
  );
4754
4822
  process.exit(1);
4755
4823
  return;
@@ -4772,14 +4840,18 @@ function registerRollback(program2) {
4772
4840
  const sha1 = createHash("sha1").update(buf).digest("hex");
4773
4841
  if (sha1 !== entry.sha1) {
4774
4842
  console.error(
4775
- `error: backup checksum mismatch for ${entry.target} (expected ${entry.sha1.slice(0, 12)}, got ${sha1.slice(0, 12)})`
4843
+ t("rollback.checksum_mismatch", {
4844
+ target: entry.target,
4845
+ expected: entry.sha1.slice(0, 12),
4846
+ actual: sha1.slice(0, 12)
4847
+ })
4776
4848
  );
4777
4849
  process.exit(1);
4778
4850
  return;
4779
4851
  }
4780
4852
  await writeFile(entry.target, normalizeEol(buf, entry.eol));
4781
4853
  }
4782
- console.log(`restored ${meta.files.length} file(s) from ${timestamp}`);
4854
+ console.log(t("rollback.restored", { count: meta.files.length, timestamp }));
4783
4855
  });
4784
4856
  }
4785
4857
 
@@ -4856,6 +4928,221 @@ async function atomicWrite(path, content) {
4856
4928
  return `write ${path} failed: ${err2.message}`;
4857
4929
  }
4858
4930
  }
4931
+
4932
+ // src/cli/lib/enableUserLangInSettings.ts
4933
+ init_harnessedRoot();
4934
+ function settingsPath2() {
4935
+ return resolve(homedir(), ".claude", "settings.json");
4936
+ }
4937
+ function detectUserLang(override) {
4938
+ if (override) {
4939
+ if (/^zh([^a-z]|$)/i.test(override)) return "zh-Hans";
4940
+ return "en";
4941
+ }
4942
+ const raw = process.env.HARNESSED_LANG || process.env.LC_ALL || process.env.LANG || process.env.LANGUAGE || safeIntlLocale2() || "";
4943
+ if (/^zh([^a-z]|$)/i.test(raw)) return "zh-Hans";
4944
+ return "en";
4945
+ }
4946
+ function safeIntlLocale2() {
4947
+ try {
4948
+ return Intl.DateTimeFormat().resolvedOptions().locale;
4949
+ } catch {
4950
+ return void 0;
4951
+ }
4952
+ }
4953
+ async function enableUserLangInSettings(override) {
4954
+ const path = settingsPath2();
4955
+ const detected = detectUserLang(override);
4956
+ let raw;
4957
+ try {
4958
+ raw = await readFile(path, "utf8");
4959
+ } catch (err2) {
4960
+ const code = err2.code;
4961
+ if (code !== "ENOENT") {
4962
+ return { status: "warn", message: `read ${path} failed: ${err2.message}` };
4963
+ }
4964
+ return createFreshSettings2(path, detected);
4965
+ }
4966
+ let data;
4967
+ try {
4968
+ const parsed = JSON.parse(raw);
4969
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4970
+ return { status: "warn", message: `${path} is not a JSON object` };
4971
+ }
4972
+ data = parsed;
4973
+ } catch (err2) {
4974
+ return { status: "warn", message: `${path} malformed JSON: ${err2.message}` };
4975
+ }
4976
+ const env = data.env ?? {};
4977
+ const existing = env.HARNESSED_USER_LANG;
4978
+ if (typeof existing === "string" && existing.length > 0 && override === void 0) {
4979
+ return { status: "already-set", path, existing };
4980
+ }
4981
+ const backupPath = await backupOriginal2(raw);
4982
+ if (backupPath.status === "warn") return backupPath;
4983
+ data.env = { ...env, HARNESSED_USER_LANG: detected };
4984
+ const writeErr = await atomicWrite2(path, `${JSON.stringify(data, null, 2)}
4985
+ `);
4986
+ if (writeErr) return { status: "warn", message: writeErr };
4987
+ return { status: "enabled", path, backupPath: backupPath.path, detected };
4988
+ }
4989
+ async function createFreshSettings2(path, detected) {
4990
+ const data = { env: { HARNESSED_USER_LANG: detected } };
4991
+ try {
4992
+ await mkdir(join(homedir(), ".claude"), { recursive: true });
4993
+ } catch (err2) {
4994
+ return { status: "warn", message: `mkdir ~/.claude failed: ${err2.message}` };
4995
+ }
4996
+ const writeErr = await atomicWrite2(path, `${JSON.stringify(data, null, 2)}
4997
+ `);
4998
+ if (writeErr) return { status: "warn", message: writeErr };
4999
+ return { status: "created", path, detected };
5000
+ }
5001
+ async function backupOriginal2(raw) {
5002
+ const backupRoot = harnessedSubdir("backups");
5003
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
5004
+ const backupPath = join(backupRoot, `settings.json.${ts}.bak`);
5005
+ try {
5006
+ await mkdir(backupRoot, { recursive: true });
5007
+ await writeFile(backupPath, raw, "utf8");
5008
+ return { status: "ok", path: backupPath };
5009
+ } catch (err2) {
5010
+ return { status: "warn", message: `backup ${backupPath} failed: ${err2.message}` };
5011
+ }
5012
+ }
5013
+ async function atomicWrite2(path, content) {
5014
+ const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`;
5015
+ try {
5016
+ await writeFile(tmpPath, content, "utf8");
5017
+ await rename(tmpPath, path);
5018
+ return void 0;
5019
+ } catch (err2) {
5020
+ return `write ${path} failed: ${err2.message}`;
5021
+ }
5022
+ }
5023
+ function readInstalledPlugins(homedirOverride) {
5024
+ const home = homedir();
5025
+ const path = join(home, ".claude", "plugins", "installed_plugins.json");
5026
+ let raw;
5027
+ try {
5028
+ raw = readFileSync(path, "utf8");
5029
+ } catch {
5030
+ return /* @__PURE__ */ new Set();
5031
+ }
5032
+ let parsed;
5033
+ try {
5034
+ parsed = JSON.parse(raw);
5035
+ } catch {
5036
+ return /* @__PURE__ */ new Set();
5037
+ }
5038
+ if (!parsed || typeof parsed !== "object") return /* @__PURE__ */ new Set();
5039
+ const plugins = parsed.plugins;
5040
+ if (!plugins || typeof plugins !== "object") return /* @__PURE__ */ new Set();
5041
+ const out = /* @__PURE__ */ new Set();
5042
+ for (const key of Object.keys(plugins)) {
5043
+ const at = key.indexOf("@");
5044
+ if (at <= 0) continue;
5045
+ out.add(key.slice(0, at));
5046
+ }
5047
+ return out;
5048
+ }
5049
+ function resolveCapabilityCmd(capability, installedPlugins) {
5050
+ const { cmd, plugin_namespace } = capability;
5051
+ if (!plugin_namespace) return { renderedCmd: cmd };
5052
+ if (cmd.includes(":")) return { renderedCmd: cmd };
5053
+ if (!cmd.startsWith("/")) return { renderedCmd: cmd };
5054
+ if (!installedPlugins.has(plugin_namespace)) {
5055
+ return {
5056
+ renderedCmd: cmd,
5057
+ warning: `plugin '${plugin_namespace}' not installed \u2014 '${cmd}' will not resolve via Claude Code. install: 'claude plugin install ${plugin_namespace}' or see plugin docs.`
5058
+ };
5059
+ }
5060
+ const bare = cmd.slice(1);
5061
+ return { renderedCmd: `/${plugin_namespace}:${bare}` };
5062
+ }
5063
+ var CAPABILITY_CMD_TEMPLATE = /\{\{\s*capabilities\.([a-zA-Z0-9_-]+)\.cmd\s*\}\}/g;
5064
+ function renderSkillBody(body, capabilities, installedPlugins) {
5065
+ const warningsSet = /* @__PURE__ */ new Set();
5066
+ const out = body.replace(CAPABILITY_CMD_TEMPLATE, (match2, name) => {
5067
+ const cap = capabilities[name];
5068
+ if (!cap) {
5069
+ warningsSet.add(
5070
+ `capability '${name}' referenced in SKILL.md but not defined in capabilities.yaml`
5071
+ );
5072
+ return match2;
5073
+ }
5074
+ const { renderedCmd, warning } = resolveCapabilityCmd(cap, installedPlugins);
5075
+ if (warning) warningsSet.add(warning);
5076
+ return renderedCmd;
5077
+ });
5078
+ return { body: out, warnings: [...warningsSet] };
5079
+ }
5080
+
5081
+ // src/cli/lib/renderSkillTemplates.ts
5082
+ async function loadCapabilities(workflowsDir) {
5083
+ const path = join(workflowsDir, "capabilities.yaml");
5084
+ const raw = await readFile(path, "utf8");
5085
+ const doc = parse(raw);
5086
+ return doc?.capabilities ?? {};
5087
+ }
5088
+ async function renderSkillFile(skillName, skillsBase, capabilities, installedPlugins) {
5089
+ const skillPath = join(skillsBase, skillName, "SKILL.md");
5090
+ const result = {
5091
+ name: skillName,
5092
+ skillPath,
5093
+ rendered: false,
5094
+ warnings: []
5095
+ };
5096
+ let body;
5097
+ try {
5098
+ body = await readFile(skillPath, "utf8");
5099
+ } catch (e) {
5100
+ result.error = `read failed: ${e.message}`;
5101
+ return result;
5102
+ }
5103
+ const rendered = renderSkillBody(body, capabilities, installedPlugins);
5104
+ if (rendered.body === body) {
5105
+ result.warnings = rendered.warnings;
5106
+ return result;
5107
+ }
5108
+ try {
5109
+ await writeFile(skillPath, rendered.body, "utf8");
5110
+ result.rendered = true;
5111
+ result.warnings = rendered.warnings;
5112
+ } catch (e) {
5113
+ result.error = `write failed: ${e.message}`;
5114
+ }
5115
+ return result;
5116
+ }
5117
+ async function renderAllSkills(skillNames, skillsBase, workflowsDir, homedirOverride) {
5118
+ let capabilities = {};
5119
+ try {
5120
+ capabilities = await loadCapabilities(workflowsDir);
5121
+ } catch (e) {
5122
+ return {
5123
+ results: skillNames.map((name) => ({
5124
+ name,
5125
+ skillPath: join(skillsBase, name, "SKILL.md"),
5126
+ rendered: false,
5127
+ warnings: [],
5128
+ error: `capabilities.yaml load failed: ${e.message}`
5129
+ })),
5130
+ aggregatedWarnings: [
5131
+ `capabilities.yaml unreadable \u2014 SKILL.md placeholders left verbatim (${e.message})`
5132
+ ]
5133
+ };
5134
+ }
5135
+ const installedPlugins = readInstalledPlugins();
5136
+ const results = [];
5137
+ const warningSet = /* @__PURE__ */ new Set();
5138
+ for (const name of skillNames) {
5139
+ const r = await renderSkillFile(name, skillsBase, capabilities, installedPlugins);
5140
+ results.push(r);
5141
+ for (const w of r.warnings) warningSet.add(w);
5142
+ if (r.error) warningSet.add(`${name}: ${r.error}`);
5143
+ }
5144
+ return { results, aggregatedWarnings: [...warningSet] };
5145
+ }
4859
5146
  init_checkAgentTeams();
4860
5147
  var FLAT_LEGACY_DEPRECATED = /* @__PURE__ */ new Set(["plan-feature", "execute-task", "verify-work"]);
4861
5148
  var FLAT_LEGACY_KEEP = /* @__PURE__ */ new Set(["research", "retro", "auto"]);
@@ -5026,7 +5313,10 @@ async function listBaseManifests2(pkgRoot) {
5026
5313
  function registerSetup(program2) {
5027
5314
  program2.command("setup").description(
5028
5315
  "One-shot onboarding: install workflow skills + base manifests to ~/.claude/ (immediate by default \u2014 use --dry-run for preview)"
5029
- ).option("--dry-run", "preview only \u2014 do not write to disk (opt-in for advanced users)").action(async (raw) => {
5316
+ ).option("--dry-run", "preview only \u2014 do not write to disk (opt-in for advanced users)").option(
5317
+ "--user-lang <code>",
5318
+ "override detected OS locale for env.HARNESSED_USER_LANG (en | zh-Hans / zh-CN / zh-TW)"
5319
+ ).action(async (raw) => {
5030
5320
  const dryRun = raw.dryRun === true;
5031
5321
  const pkgRoot = getPackageRoot();
5032
5322
  const workflowsDir = resolve(pkgRoot, "workflows");
@@ -5036,7 +5326,7 @@ function registerSetup(program2) {
5036
5326
  try {
5037
5327
  entries = await readdir(workflowsDir);
5038
5328
  } catch {
5039
- console.error(`error: workflows directory not found at ${workflowsDir}`);
5329
+ console.error(t("setup.workflows_not_found", { path: workflowsDir }));
5040
5330
  process.exit(1);
5041
5331
  }
5042
5332
  const { workflows: toInstall, deprecated } = await scanWorkflowsWithSkill(
@@ -5046,18 +5336,16 @@ function registerSetup(program2) {
5046
5336
  const depBlock = renderDeprecationBlock(deprecated);
5047
5337
  if (depBlock) console.log(depBlock);
5048
5338
  if (toInstall.length === 0) {
5049
- console.log("setup: no workflow directories with SKILL.md found \u2014 nothing to install");
5339
+ console.log(t("setup.nothing_to_install"));
5050
5340
  process.exit(2);
5051
5341
  }
5052
5342
  if (dryRun) {
5053
- console.log(
5054
- `[dry-run] setup would install ${toInstall.length} workflow(s) to ${skillsBase}:`
5055
- );
5343
+ console.log(t("setup.dry_run.header", { count: toInstall.length, path: skillsBase }));
5056
5344
  for (const wf of toInstall) {
5057
5345
  const masterTag = wf.isMaster ? " (master)" : "";
5058
5346
  console.log(` ${wf.name} \u2192 ${join(skillsBase, wf.name)}${masterTag}`);
5059
5347
  }
5060
- console.log(` run without --dry-run to execute`);
5348
+ console.log(t("setup.dry_run.run_hint"));
5061
5349
  process.exit(0);
5062
5350
  }
5063
5351
  let skillsInstalled = 0;
@@ -5070,31 +5358,68 @@ function registerSetup(program2) {
5070
5358
  console.log(` [A] installed ${wf.name} \u2192 ${dst}${masterTag}`);
5071
5359
  skillsInstalled++;
5072
5360
  } catch (e) {
5073
- console.error(` error: failed to copy ${wf.name}: ${e.message}`);
5361
+ console.error(t("setup.copy_failed", { name: wf.name, message: e.message }));
5074
5362
  process.exit(1);
5075
5363
  }
5076
5364
  }
5365
+ console.log(t("setup.step_a_complete", { count: skillsInstalled, path: skillsBase }));
5366
+ const skillNames = toInstall.map((wf) => wf.name);
5367
+ const rendered = await renderAllSkills(skillNames, skillsBase, workflowsDir);
5368
+ const renderedCount = rendered.results.filter((r) => r.rendered).length;
5077
5369
  console.log(
5078
- `
5079
- Step A complete: ${skillsInstalled} workflow skill(s) installed to ${skillsBase}`
5370
+ t("setup.step_a_render.complete", {
5371
+ count: renderedCount,
5372
+ total: skillsInstalled
5373
+ })
5080
5374
  );
5375
+ if (rendered.aggregatedWarnings.length > 0) {
5376
+ console.warn(t("setup.step_a_render.warnings_header"));
5377
+ for (const w of rendered.aggregatedWarnings) {
5378
+ console.warn(` - ${w}`);
5379
+ }
5380
+ }
5081
5381
  const cResult = await enableAgentTeamsInSettings();
5082
5382
  if (cResult.status === "created") {
5083
- console.log(` [C] created ${cResult.path} + enabled Agent Teams`);
5383
+ console.log(t("setup.step_c.created", { path: cResult.path }));
5084
5384
  } else if (cResult.status === "already-enabled") {
5085
- console.log(` [C] Agent Teams already enabled (${cResult.path})`);
5385
+ console.log(t("setup.step_c.already_enabled", { path: cResult.path }));
5086
5386
  } else if (cResult.status === "enabled") {
5087
5387
  console.log(
5088
- ` [C] enabled Agent Teams in ${cResult.path} (backup saved \u2192 ${cResult.backupPath})`
5388
+ t("setup.step_c.enabled_backup", {
5389
+ path: cResult.path,
5390
+ backupPath: cResult.backupPath
5391
+ })
5089
5392
  );
5090
5393
  } else {
5091
- console.warn(` [C] Agent Teams enable skipped: ${cResult.message}`);
5394
+ console.warn(t("setup.step_c.skipped", { message: cResult.message }));
5395
+ }
5396
+ const dResult = await enableUserLangInSettings(raw.userLang);
5397
+ if (dResult.status === "created") {
5398
+ console.log(t("setup.step_d.created", { path: dResult.path, lang: dResult.detected }));
5399
+ } else if (dResult.status === "already-set") {
5400
+ console.log(t("setup.step_d.already_set", { path: dResult.path, lang: dResult.existing }));
5401
+ } else if (dResult.status === "enabled") {
5402
+ console.log(
5403
+ t("setup.step_d.enabled_backup", {
5404
+ path: dResult.path,
5405
+ lang: dResult.detected,
5406
+ backupPath: dResult.backupPath
5407
+ })
5408
+ );
5409
+ } else {
5410
+ console.warn(t("setup.step_d.skipped", { message: dResult.message }));
5092
5411
  }
5093
5412
  const manifestPaths = await listBaseManifests2(pkgRoot);
5094
5413
  const b = await runStepBInstall(manifestPaths);
5095
5414
  const stepBMs = (b.elapsedMs / 1e3).toFixed(1);
5096
5415
  console.log(
5097
- `Step B complete: ${b.installed.length} manifest(s) installed / ${b.alreadyInstalled.length} already-installed / ${b.skipped.length} skipped / ${b.failed.length} failed [parallel ${stepBMs}s]`
5416
+ t("setup.step_b_complete", {
5417
+ installed: b.installed.length,
5418
+ already: b.alreadyInstalled.length,
5419
+ skipped: b.skipped.length,
5420
+ failed: b.failed.length,
5421
+ seconds: stepBMs
5422
+ })
5098
5423
  );
5099
5424
  for (const n of b.installed) console.log(` [B] installed ${n}`);
5100
5425
  for (const n of b.alreadyInstalled)
@@ -5104,41 +5429,33 @@ Step A complete: ${skillsInstalled} workflow skill(s) installed to ${skillsBase}
5104
5429
  for (const n of b.skipped) console.log(` [B] skipped ${n}`);
5105
5430
  for (const n of b.failed) console.error(` [B] failed ${n}`);
5106
5431
  console.log(
5107
- `
5108
- setup complete: ${skillsInstalled} workflow skill(s) + ${b.installed.length + b.alreadyInstalled.length} base manifest(s) configured`
5432
+ t("setup.complete", {
5433
+ skills: skillsInstalled,
5434
+ manifests: b.installed.length + b.alreadyInstalled.length
5435
+ })
5109
5436
  );
5110
5437
  if (b.alreadyInstalled.length > 0 || b.installed.length > 0) {
5111
- console.log(
5112
- `
5113
- MCP servers configured. Run \`/mcp\` in Claude Code to verify each server's connection status. If a server shows disconnected, restart Claude Code or check the MCP command spec.`
5114
- );
5438
+ console.log(t("setup.mcp_hint"));
5115
5439
  }
5116
- console.log(
5117
- "\n\u2713 harnessed v3.0 \u4E09\u5C42\u6808\u65B9\u6CD5\u8BBA bundled \u2014 23 workflows (4 master + 18 sub + 1 standalone) + 6 disciplines + 10 judgments + ~83 capabilities ready"
5118
- );
5119
- console.log(
5120
- " workflows in <packageRoot>/workflows/ (Pure bundled, NOT user-dir override per D-01)"
5121
- );
5440
+ console.log(t("setup.bundled_summary"));
5441
+ console.log(t("setup.bundled_location"));
5122
5442
  process.exit(0);
5123
5443
  });
5124
5444
  }
5125
-
5126
- // src/cli/status.ts
5127
5445
  init_harnessedRoot();
5128
5446
  function registerStatus(program2) {
5129
5447
  program2.command("status").description("Show installed upstreams (from <harnessed-root>/state.json)").action(async () => {
5130
5448
  const state = await readState(process.cwd());
5131
5449
  const names = Object.keys(state.installed).sort();
5132
5450
  if (names.length === 0) {
5133
- console.log(`no installs recorded (${harnessedFile("state.json")} absent or empty)`);
5451
+ console.log(t("status.no_installs", { path: harnessedFile("state.json") }));
5134
5452
  } else {
5135
5453
  for (const n of names) {
5136
5454
  const e = state.installed[n];
5137
5455
  if (!e) continue;
5138
5456
  console.log(`${n} @ ${e.version} (installed ${e.installedAt})`);
5139
5457
  }
5140
- console.log(`
5141
- ${names.length} install${names.length === 1 ? "" : "s"} recorded`);
5458
+ console.log(t("status.summary_installs", { count: names.length }));
5142
5459
  }
5143
5460
  const lockPath = harnessedFile(".lock");
5144
5461
  try {
@@ -5150,18 +5467,20 @@ ${names.length} install${names.length === 1 ? "" : "s"} recorded`);
5150
5467
  const s = await stat(lockPath);
5151
5468
  const ageMs = Date.now() - s.mtime.getTime();
5152
5469
  const stale = ageMs > 1e4;
5153
- console.log(`
5154
- lock: held (since ${s.mtime.toISOString()})${stale ? " \u2014 STALE" : ""}`);
5155
- console.log(` to release: wait for process to finish or delete ${lockPath}`);
5470
+ console.log(
5471
+ t("status.lock_held", {
5472
+ since: s.mtime.toISOString(),
5473
+ staleSuffix: stale ? t("status.lock_held.stale_suffix") : ""
5474
+ })
5475
+ );
5476
+ console.log(t("status.lock_release_hint", { path: lockPath }));
5156
5477
  } else {
5157
- console.log("\nlock: free");
5478
+ console.log(t("status.lock_free"));
5158
5479
  }
5159
5480
  } catch {
5160
5481
  }
5161
5482
  });
5162
5483
  }
5163
-
5164
- // src/cli/uninstall.ts
5165
5484
  init_path_guard();
5166
5485
 
5167
5486
  // src/uninstallers/lib/runOrPreview.ts
@@ -5178,10 +5497,10 @@ var uninstallCcHookAdd = async (ctx) => {
5178
5497
  }
5179
5498
  const abort = dryRunGate(ctx);
5180
5499
  if (abort) return abort;
5181
- const settingsPath2 = join(homedir(), ".claude", "settings.json");
5500
+ const settingsPath3 = join(homedir(), ".claude", "settings.json");
5182
5501
  let existing;
5183
5502
  try {
5184
- existing = await readFile(settingsPath2, "utf8");
5503
+ existing = await readFile(settingsPath3, "utf8");
5185
5504
  } catch {
5186
5505
  return { ok: true, removedPaths: [] };
5187
5506
  }
@@ -5210,8 +5529,8 @@ var uninstallCcHookAdd = async (ctx) => {
5210
5529
  if (settings.hooks?.[ev]?.length === before || before === settings.hooks?.[ev]?.length) ;
5211
5530
  const newText = `${JSON.stringify(settings, null, 2)}
5212
5531
  `;
5213
- await writeFile(settingsPath2, newText);
5214
- return { ok: true, removedPaths: [settingsPath2] };
5532
+ await writeFile(settingsPath3, newText);
5533
+ return { ok: true, removedPaths: [settingsPath3] };
5215
5534
  };
5216
5535
 
5217
5536
  // src/uninstallers/ccPluginMarketplace.ts
@@ -5253,11 +5572,11 @@ function extractCloneTarget2(cmd) {
5253
5572
  const tokens = tail.split(/\s+/);
5254
5573
  let i = 0;
5255
5574
  while (i < tokens.length) {
5256
- const t = tokens[i];
5257
- if (t === void 0 || !t.startsWith("-")) break;
5258
- if (t === "--depth" || t === "--branch" || t === "-b") {
5575
+ const t2 = tokens[i];
5576
+ if (t2 === void 0 || !t2.startsWith("-")) break;
5577
+ if (t2 === "--depth" || t2 === "--branch" || t2 === "-b") {
5259
5578
  i += 2;
5260
- } else if (t.includes("=")) {
5579
+ } else if (t2.includes("=")) {
5261
5580
  i += 1;
5262
5581
  } else {
5263
5582
  i += 2;
@@ -5347,7 +5666,7 @@ var uninstallNpmCli = async (ctx) => {
5347
5666
  const m = install.cmd.match(/npm\s+(?:install|i)\s+(?:-g\s+)?(\S+)/);
5348
5667
  const pkg = m?.[1] ?? ctx.manifest.metadata.upstream.source;
5349
5668
  const isWin = process.platform === "win32";
5350
- const result = await new Promise((resolve10) => {
5669
+ const result = await new Promise((resolve12) => {
5351
5670
  const child = isWin ? spawn("cmd.exe", ["/c", "npm", "uninstall", "-g", pkg], { windowsHide: true }) : spawn("npm", ["uninstall", "-g", pkg], { shell: false });
5352
5671
  let stderr = "";
5353
5672
  child.stderr?.setEncoding("utf8").on("data", (c) => {
@@ -5355,15 +5674,15 @@ var uninstallNpmCli = async (ctx) => {
5355
5674
  });
5356
5675
  const timer = setTimeout(() => {
5357
5676
  child.kill("SIGKILL");
5358
- resolve10({ exitCode: -1, stderr: `${stderr}[timeout]` });
5677
+ resolve12({ exitCode: -1, stderr: `${stderr}[timeout]` });
5359
5678
  }, 3e4);
5360
5679
  child.on("error", (e) => {
5361
5680
  clearTimeout(timer);
5362
- resolve10({ exitCode: -1, stderr: e.message });
5681
+ resolve12({ exitCode: -1, stderr: e.message });
5363
5682
  });
5364
5683
  child.on("close", (code) => {
5365
5684
  clearTimeout(timer);
5366
- resolve10({ exitCode: code ?? -1, stderr });
5685
+ resolve12({ exitCode: code ?? -1, stderr });
5367
5686
  });
5368
5687
  });
5369
5688
  if (result.exitCode !== 0) {
@@ -5417,8 +5736,8 @@ function registerUninstall(program2) {
5417
5736
  const yes = raw.yes === true || raw.nonInteractive === true;
5418
5737
  if (yes && raw.dryRun) {
5419
5738
  console.error(
5420
- `error: --yes is incompatible with --dry-run (dry-run does not mutate)
5421
- fix: harnessed uninstall ${name} --yes (immediate) OR harnessed uninstall ${name} --dry-run (preview)`
5739
+ `${t("uninstall.yes_dryrun_conflict")}
5740
+ ${t("uninstall.yes_dryrun_conflict.fix", { name })}`
5422
5741
  );
5423
5742
  process.exit(2);
5424
5743
  }
@@ -5437,8 +5756,8 @@ function registerUninstall(program2) {
5437
5756
  chosenPath = skillPackPath;
5438
5757
  } catch {
5439
5758
  console.error(
5440
- `error: manifest '${resolvedName}' not found
5441
- fix: ensure manifests/tools/${resolvedName}.yaml or manifests/skill-packs/${resolvedName}.yaml exists`
5759
+ `${t("install.manifest_not_found", { name: resolvedName })}
5760
+ ${t("install.manifest_not_found.fix", { name: resolvedName })}`
5442
5761
  );
5443
5762
  process.exit(1);
5444
5763
  }
@@ -5451,28 +5770,28 @@ function registerUninstall(program2) {
5451
5770
  const method = v.manifest.spec.install.method;
5452
5771
  const dryRun = raw.dryRun === true;
5453
5772
  if (dryRun) {
5454
- console.log(`[dry-run] would uninstall '${resolvedName}' via method '${method}'`);
5455
- console.log(` run without --dry-run to execute`);
5773
+ console.log(t("uninstall.dry_run.preview", { name: resolvedName, method }));
5774
+ console.log(t("uninstall.dry_run.run_hint"));
5456
5775
  process.exit(2);
5457
5776
  }
5458
5777
  if (!yes) {
5459
5778
  const answer = await p.confirm({
5460
- message: `Uninstall '${resolvedName}'? This cannot be undone.`,
5779
+ message: t("uninstall.confirm.prompt", { name: resolvedName }),
5461
5780
  initialValue: false
5462
5781
  });
5463
5782
  if (p.isCancel(answer) || answer === false) {
5464
- console.error(`aborted: user cancelled`);
5783
+ console.error(t("uninstall.cancelled"));
5465
5784
  process.exit(2);
5466
5785
  }
5467
5786
  }
5468
5787
  const opts = { apply: true, dryRun: false, yes };
5469
5788
  const result = await runUninstall(v.manifest, opts);
5470
5789
  if ("aborted" in result) {
5471
- console.error(`aborted: ${result.reason}`);
5790
+ console.error(t("install.aborted", { reason: result.reason }));
5472
5791
  process.exit(2);
5473
5792
  }
5474
5793
  if (result.ok) {
5475
- console.log(`uninstalled ${resolvedName}`);
5794
+ console.log(t("uninstall.completed", { name: resolvedName }));
5476
5795
  process.exit(0);
5477
5796
  }
5478
5797
  console.error(`error: ${result.error}`);
@@ -5483,8 +5802,20 @@ function registerUninstall(program2) {
5483
5802
  // src/cli.ts
5484
5803
  init_harnessedRoot();
5485
5804
  migrateLegacyHarnessedRoot();
5805
+ var argv = process.argv;
5806
+ for (let i = 2; i < argv.length; i++) {
5807
+ const a = argv[i];
5808
+ if (a === "--lang" && i + 1 < argv.length) {
5809
+ setLocale(argv[i + 1]);
5810
+ break;
5811
+ }
5812
+ if (a?.startsWith("--lang=")) {
5813
+ setLocale(a.slice("--lang=".length));
5814
+ break;
5815
+ }
5816
+ }
5486
5817
  var program = new Command();
5487
- program.name("harnessed").description("AI coding harness package manager + composition orchestrator").version(package_default.version);
5818
+ program.name("harnessed").description("AI coding harness package manager + composition orchestrator").version(package_default.version).option("--lang <code>", "output language: en | zh (auto-detect from $LANG / Intl if absent)");
5488
5819
  registerInstall(program);
5489
5820
  registerInstallBase(program);
5490
5821
  registerResearch(program);