harness-bujang 0.3.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +46 -4
  2. package/dist/index.js +470 -81
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,11 +10,11 @@ Install the [Harness-Bujang](https://github.com/bjcho4141/harness-bujang) multi-
10
10
  # Interactive setup — prompts for language, backend, etc.
11
11
  npx harness-bujang init
12
12
 
13
- # Non-interactive (CI / scripts) — accept all defaults
13
+ # Non-interactive (CI / scripts) — accept all defaults (Korean agents)
14
14
  npx harness-bujang init --yes
15
15
 
16
- # Korean agents (full 부장 persona)
17
- npx harness-bujang init --lang=ko
16
+ # English agents (default is Korean from 0.4.2+)
17
+ npx harness-bujang init --lang=en
18
18
 
19
19
  # Different folder, skip the chat-room UI
20
20
  npx harness-bujang init --target=./my-app --no-template
@@ -52,7 +52,7 @@ npx harness-bujang chat --create
52
52
  npx harness-bujang init [options]
53
53
 
54
54
  Options:
55
- --lang=<ko|en> Agent language (default: en)
55
+ --lang=<ko|en> Agent language (default: ko — full 부장 persona)
56
56
  --target=<path> Project root (default: .)
57
57
  --framework=<name> Override detected framework
58
58
  --db=<name> Override detected DB
@@ -93,6 +93,48 @@ that any agent can pick up next time they read the chat.
93
93
  Requires the `sqlite3` command-line tool (preinstalled on macOS; `apt-get install
94
94
  sqlite3` on Ubuntu/WSL; sqlite-tools binaries on Windows).
95
95
 
96
+ ### `adapt`
97
+
98
+ ```
99
+ npx harness-bujang adapt --to=<target> [options]
100
+
101
+ Targets:
102
+ cursor → .cursor/rules/bujang-*.mdc (Cursor IDE)
103
+ cline → .clinerules/bujang-*.md (Cline)
104
+ aider → CONVENTIONS.md + .aider.conf.yml (Aider)
105
+ codex → AGENTS.md (OpenAI Codex CLI / Copilot Coding Agent / Cody)
106
+ gemini → GEMINI.md + .gemini/styleguide.md (Antigravity / Gemini CLI / Code Assist)
107
+ all → all of the above
108
+ ```
109
+
110
+ Converts the canonical `.claude/agents/*.md` install into the file formats other
111
+ editor / agent harness tools expect. The `.claude/agents/` directory remains the
112
+ single source of truth — re-run `bujang adapt --to=<target>` after changes to
113
+ keep adapters in sync.
114
+
115
+ Examples:
116
+
117
+ ```bash
118
+ npx harness-bujang adapt --to=cursor # just Cursor
119
+ npx harness-bujang adapt --to=cursor,aider # multiple
120
+ npx harness-bujang adapt --to=all # everything
121
+ ```
122
+
123
+ Tools covered (5 adapter formats → 8+ tools):
124
+
125
+ | Tool | File the adapter writes |
126
+ |------|-------------------------|
127
+ | Cursor IDE | `.cursor/rules/bujang-*.mdc` (with frontmatter) |
128
+ | Cline | `.clinerules/bujang-*.md` |
129
+ | Aider | `CONVENTIONS.md` + `.aider.conf.yml` (`read:`) |
130
+ | OpenAI Codex CLI | `AGENTS.md` |
131
+ | GitHub Copilot Coding Agent | `AGENTS.md` |
132
+ | Sourcegraph Cody | `AGENTS.md` (recent versions) |
133
+ | Google Antigravity | `GEMINI.md` (highest priority) + falls back to `AGENTS.md` |
134
+ | Gemini CLI | `GEMINI.md` |
135
+ | Gemini Code Assist (workspace) | `GEMINI.md` (precedence) + `.gemini/styleguide.md` |
136
+ | Gemini Code Assist (GitHub PR review) | `.gemini/styleguide.md` |
137
+
96
138
  ## How the harness works once installed
97
139
 
98
140
  ```
package/dist/index.js CHANGED
@@ -380,10 +380,13 @@ async function runInit(args) {
380
380
  printBackendInstructions(opts.chatBackend, opts.commitChat);
381
381
  } else {
382
382
  console.log(
383
- `${c.yellow("\u26A0 Chat-room UI is Next.js only.")} ` + c.dim(`Skipping \u2014 your stack is detected as ${scan.framework}.`)
383
+ `${c.yellow("\u2139\uFE0E Chat-room UI (Next.js admin route) skipped")} ` + c.dim(`\u2014 your stack is detected as ${scan.framework}.`)
384
384
  );
385
385
  console.log(
386
- c.dim(" (For non-Next.js stacks, use the agents only \u2014 chat-room support is on the roadmap.)")
386
+ ` ${c.dim("To use the chat room on this stack, run")} ${c.bold("bujang chat")} ${c.dim("\u2014 it serves the")}`
387
+ );
388
+ console.log(
389
+ ` ${c.dim("same KakaoTalk-style viewer at http://localhost:7777, no Next.js needed.")}`
387
390
  );
388
391
  console.log();
389
392
  }
@@ -394,15 +397,19 @@ async function runInit(args) {
394
397
  console.log(` ${c.cyan("1.")} Open Claude Code in this project`);
395
398
  console.log(` ${c.cyan("2.")} Run ${c.bold("/bujang-status")} (if the plugin is installed) or just`);
396
399
  console.log(` ask ${c.bold('"Director, please add a hello-world endpoint"')}`);
397
- console.log(` ${c.cyan("3.")} Watch ${c.bold(context.ADMIN_HARNESS_ROUTE)} for live updates (after env setup)`);
400
+ if (scan.framework.startsWith("Next.js") && opts.installTemplate) {
401
+ console.log(` ${c.cyan("3.")} Watch ${c.bold(context.ADMIN_HARNESS_ROUTE)} for live updates (after env setup)`);
402
+ } else {
403
+ console.log(` ${c.cyan("3.")} Watch the chat room: ${c.bold("npx harness-bujang chat --create")} ${c.dim("\u2192 http://localhost:7777")}`);
404
+ }
398
405
  console.log();
399
406
  }
400
407
  async function promptInteractive(opts, scan) {
401
408
  const lang = await select({
402
- message: "Agent language",
409
+ message: "Agent language / \uC5D0\uC774\uC804\uD2B8 \uC5B8\uC5B4",
403
410
  choices: [
404
- { name: "English", value: "en" },
405
- { name: "Korean \u2014 full \uBD80\uC7A5 persona (\uD55C\uAD6D\uC5B4)", value: "ko" }
411
+ { name: "\uD55C\uAD6D\uC5B4 \u2014 full \uBD80\uC7A5 persona (Korean)", value: "ko" },
412
+ { name: "English", value: "en" }
406
413
  ],
407
414
  default: opts.lang
408
415
  });
@@ -424,7 +431,7 @@ async function promptInteractive(opts, scan) {
424
431
  return { ...opts, lang, chatBackend, installTemplate };
425
432
  }
426
433
  function parseArgs(args) {
427
- const lang = getFlag(args, "--lang") ?? "en";
434
+ const lang = getFlag(args, "--lang") ?? "ko";
428
435
  if (!["ko", "en"].includes(lang)) {
429
436
  throw new Error(`--lang must be "ko" or "en", got "${lang}"`);
430
437
  }
@@ -855,11 +862,11 @@ async function upsertEnvVar(envFile, key, value) {
855
862
  }
856
863
  async function confirm2(message) {
857
864
  process.stdout.write(`${message} [y/N] `);
858
- return new Promise((resolve5) => {
865
+ return new Promise((resolve6) => {
859
866
  process.stdin.setEncoding("utf8");
860
867
  process.stdin.once("data", (chunk) => {
861
868
  const ans = chunk.toString().trim().toLowerCase();
862
- resolve5(ans === "y" || ans === "yes");
869
+ resolve6(ans === "y" || ans === "yes");
863
870
  process.stdin.pause();
864
871
  });
865
872
  });
@@ -1002,7 +1009,7 @@ async function runChat(args) {
1002
1009
  res.writeHead(404);
1003
1010
  res.end("not found");
1004
1011
  });
1005
- await new Promise((resolve5) => server.listen(port, "127.0.0.1", resolve5));
1012
+ await new Promise((resolve6) => server.listen(port, "127.0.0.1", resolve6));
1006
1013
  const url = `http://localhost:${port}`;
1007
1014
  console.log();
1008
1015
  console.log(c4.bold(c4.green("\u{1F7E2} \uD558\uB124\uC2A4 \uD1A1\uBC29 viewer")) + c4.dim(" \u2014 " + url));
@@ -1054,12 +1061,12 @@ async function findOpenPort(preferred) {
1054
1061
  throw new Error(`Could not find a free port in range ${preferred}-${preferred + 19}`);
1055
1062
  }
1056
1063
  function portIsFree(port) {
1057
- return new Promise((resolve5) => {
1064
+ return new Promise((resolve6) => {
1058
1065
  const tester = http.createServer();
1059
- tester.once("error", () => resolve5(false));
1066
+ tester.once("error", () => resolve6(false));
1060
1067
  tester.once("listening", () => {
1061
1068
  tester.close();
1062
- resolve5(true);
1069
+ resolve6(true);
1063
1070
  });
1064
1071
  tester.listen(port, "127.0.0.1");
1065
1072
  });
@@ -1073,10 +1080,10 @@ function openBrowser(url) {
1073
1080
  }
1074
1081
  }
1075
1082
  function readBody(req) {
1076
- return new Promise((resolve5, reject) => {
1083
+ return new Promise((resolve6, reject) => {
1077
1084
  const chunks = [];
1078
1085
  req.on("data", (chunk) => chunks.push(chunk));
1079
- req.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
1086
+ req.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
1080
1087
  req.on("error", reject);
1081
1088
  });
1082
1089
  }
@@ -1347,17 +1354,18 @@ function render() {
1347
1354
  }
1348
1355
  html += '</div>';
1349
1356
 
1350
- // Input bar \u2014 for sending principal messages.
1351
- html += '<div class="px-4 py-3 bg-white border-t border-gray-200 flex gap-2">';
1352
- html += '<input id="msg-input" type="text" placeholder="\uBA54\uC2DC\uC9C0 \uC785\uB825 (Enter \uC804\uC1A1)" class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:border-indigo-500" />';
1353
- html += '<button id="send-btn" class="px-4 py-2 text-sm font-semibold bg-indigo-500 text-white rounded-lg hover:bg-indigo-600">\uC804\uC1A1</button>';
1357
+ // Footer \u2014 read-only viewer reminder. The viewer is a monitor for agent
1358
+ // activity; real commands go to Claude Code itself. (Auto-pickup of
1359
+ // principal messages from this viewer is on the roadmap.)
1360
+ html += '<div class="px-4 py-2 bg-white border-t border-gray-200 text-center">';
1361
+ html += '<p class="text-xs text-gray-400">\uC77D\uAE30 \uC804\uC6A9 \uBDF0\uC5B4 \u2014 \uBA85\uB839\uC740 Claude Code \uD130\uBBF8\uB110\uC5D0\uC11C \uBD80\uC7A5\uB2D8\uAED8 \uC9C1\uC811 \uB9D0\uC500\uD558\uC138\uC694</p>';
1354
1362
  html += '</div>';
1355
1363
  }
1356
1364
  html += '</div>';
1357
1365
 
1358
1366
  root.innerHTML = html;
1359
1367
 
1360
- // Re-bind handlers
1368
+ // Re-bind room-click handlers (room list).
1361
1369
  document.querySelectorAll('[data-room-id]').forEach((el) => {
1362
1370
  el.addEventListener('click', () => {
1363
1371
  state.selectedRoom = el.getAttribute('data-room-id');
@@ -1370,43 +1378,6 @@ function render() {
1370
1378
  });
1371
1379
  });
1372
1380
 
1373
- const input = document.getElementById('msg-input');
1374
- const sendBtn = document.getElementById('send-btn');
1375
- if (input && sendBtn) {
1376
- const send = async () => {
1377
- const text = input.value.trim();
1378
- if (!text) return;
1379
- input.disabled = true;
1380
- sendBtn.disabled = true;
1381
- try {
1382
- const target = state.selectedRoom === '\uB300\uD45C\uB2D8' ? '\uBD80\uC7A5' : (state.selectedRoom || '\uBD80\uC7A5');
1383
- await fetch('/api/messages', {
1384
- method: 'POST',
1385
- headers: { 'content-type': 'application/json' },
1386
- body: JSON.stringify({
1387
- from: '\uB300\uD45C\uB2D8',
1388
- to: target,
1389
- type: 'command',
1390
- message: text,
1391
- severity: 'info',
1392
- }),
1393
- });
1394
- input.value = '';
1395
- await refresh();
1396
- const conv = document.getElementById('conversation');
1397
- if (conv) conv.scrollTop = conv.scrollHeight;
1398
- } catch (e) {
1399
- alert('\uC804\uC1A1 \uC2E4\uD328: ' + e.message);
1400
- }
1401
- input.disabled = false;
1402
- sendBtn.disabled = false;
1403
- input.focus();
1404
- };
1405
- sendBtn.addEventListener('click', send);
1406
- input.addEventListener('keydown', (e) => { if (e.key === 'Enter') send(); });
1407
- input.focus();
1408
- }
1409
-
1410
1381
  // Auto-scroll the selected room to the bottom on first render of that room.
1411
1382
  const conv = document.getElementById('conversation');
1412
1383
  if (conv && conv.dataset.scrolled !== state.selectedRoom) {
@@ -1441,7 +1412,9 @@ setInterval(refresh, 2000);
1441
1412
  `
1442
1413
  );
1443
1414
 
1444
- // src/index.ts
1415
+ // src/adapt.ts
1416
+ import * as fs6 from "fs/promises";
1417
+ import * as path6 from "path";
1445
1418
  var c5 = {
1446
1419
  bold: (s) => `\x1B[1m${s}\x1B[22m`,
1447
1420
  dim: (s) => `\x1B[2m${s}\x1B[22m`,
@@ -1450,18 +1423,419 @@ var c5 = {
1450
1423
  yellow: (s) => `\x1B[33m${s}\x1B[39m`,
1451
1424
  cyan: (s) => `\x1B[36m${s}\x1B[39m`
1452
1425
  };
1426
+ async function runAdapt(args) {
1427
+ const opts = parseArgs4(args);
1428
+ const agentsDir = path6.join(opts.target, ".claude/agents");
1429
+ if (!await exists5(agentsDir)) {
1430
+ console.log();
1431
+ console.log(c5.red("\u2716 No .claude/agents/ directory found at " + agentsDir));
1432
+ console.log();
1433
+ console.log(" Run " + c5.bold("npx harness-bujang init") + " first to install the canonical agents,");
1434
+ console.log(" then re-run this command to adapt them to your editor.");
1435
+ console.log();
1436
+ process.exitCode = 1;
1437
+ return;
1438
+ }
1439
+ const agentFiles = await loadAgents(agentsDir);
1440
+ if (agentFiles.length === 0) {
1441
+ console.log();
1442
+ console.log(c5.red("\u2716 .claude/agents/ exists but contains no .md files."));
1443
+ console.log();
1444
+ process.exitCode = 1;
1445
+ return;
1446
+ }
1447
+ console.log();
1448
+ console.log(c5.bold("\u{1F501} Harness-Bujang adapt"));
1449
+ console.log(c5.dim(` Target: ${opts.target}`));
1450
+ console.log(c5.dim(` Agents: ${agentFiles.length} files at .claude/agents/`));
1451
+ console.log(c5.dim(` Adapting: ${opts.to.join(", ")}`));
1452
+ console.log();
1453
+ for (const target of opts.to) {
1454
+ if (target === "cursor") await adaptCursor(opts.target, agentFiles, opts.yes);
1455
+ if (target === "cline") await adaptCline(opts.target, agentFiles, opts.yes);
1456
+ if (target === "aider") await adaptAider(opts.target, agentFiles, opts.yes);
1457
+ if (target === "codex") await adaptCodex(opts.target, agentFiles, opts.yes);
1458
+ if (target === "gemini") await adaptGemini(opts.target, agentFiles, opts.yes);
1459
+ }
1460
+ console.log(c5.bold(c5.green("\u2705 Done.")));
1461
+ console.log();
1462
+ console.log("Next:");
1463
+ if (opts.to.includes("cursor")) {
1464
+ console.log(` ${c5.cyan("\u2022")} Cursor users: open the project \u2014 rules in ${c5.bold(".cursor/rules/")} are auto-loaded`);
1465
+ }
1466
+ if (opts.to.includes("cline")) {
1467
+ console.log(` ${c5.cyan("\u2022")} Cline users: rules in ${c5.bold(".clinerules/")} are auto-loaded by Cline`);
1468
+ }
1469
+ if (opts.to.includes("aider")) {
1470
+ console.log(` ${c5.cyan("\u2022")} Aider users: ${c5.bold("CONVENTIONS.md")} is loaded via ${c5.bold(".aider.conf.yml")} (read:)`);
1471
+ }
1472
+ if (opts.to.includes("codex")) {
1473
+ console.log(` ${c5.cyan("\u2022")} Codex / Copilot Coding Agent users: ${c5.bold("AGENTS.md")} at the project root is auto-loaded`);
1474
+ }
1475
+ if (opts.to.includes("gemini")) {
1476
+ console.log(` ${c5.cyan("\u2022")} Antigravity / Gemini CLI / Code Assist: ${c5.bold("GEMINI.md")} (highest precedence) + ${c5.bold(".gemini/styleguide.md")} (PR reviews)`);
1477
+ }
1478
+ console.log();
1479
+ console.log(c5.dim(" When you change .claude/agents/ later, re-run this command to refresh."));
1480
+ console.log();
1481
+ }
1482
+ async function adaptCursor(target, agents, overwrite) {
1483
+ const dst = path6.join(target, ".cursor/rules");
1484
+ await fs6.mkdir(dst, { recursive: true });
1485
+ console.log(c5.bold("\u{1F4C2} Cursor \u2014 .cursor/rules/"));
1486
+ for (const a of agents) {
1487
+ const file = path6.join(dst, `bujang-${a.slug}.mdc`);
1488
+ if (await exists5(file) && !overwrite) {
1489
+ console.log(` ${c5.yellow("\u26A0")} bujang-${a.slug}.mdc ${c5.dim("(exists, skipped \u2014 use --yes to overwrite)")}`);
1490
+ continue;
1491
+ }
1492
+ const description = a.frontmatter.description || `Harness-Bujang ${a.slug}`;
1493
+ const out = `---
1494
+ description: "Harness-Bujang ${a.slug}: ${escapeYamlString(description.replace(/\n/g, " ").slice(0, 240))}"
1495
+ alwaysApply: false
1496
+ ---
1497
+
1498
+ # Harness-Bujang \u2014 ${a.slug} role guide
1499
+
1500
+ > Source of truth: \`.claude/agents/${a.slug}.md\` \u2014 re-run \`bujang adapt --to=cursor\` to sync.
1501
+
1502
+ When the user request matches this role's domain (see description above), follow this guide as your primary system prompt for the response. Other rules under this directory describe sibling roles in the same harness.
1503
+
1504
+ ---
1505
+
1506
+ ` + a.body.trim() + `
1507
+ `;
1508
+ await fs6.writeFile(file, out);
1509
+ console.log(` ${c5.green("\u2713")} bujang-${a.slug}.mdc`);
1510
+ }
1511
+ console.log();
1512
+ }
1513
+ async function adaptCline(target, agents, overwrite) {
1514
+ const dst = path6.join(target, ".clinerules");
1515
+ await fs6.mkdir(dst, { recursive: true });
1516
+ console.log(c5.bold("\u{1F4C2} Cline \u2014 .clinerules/"));
1517
+ for (const a of agents) {
1518
+ const file = path6.join(dst, `bujang-${a.slug}.md`);
1519
+ if (await exists5(file) && !overwrite) {
1520
+ console.log(` ${c5.yellow("\u26A0")} bujang-${a.slug}.md ${c5.dim("(exists, skipped \u2014 use --yes to overwrite)")}`);
1521
+ continue;
1522
+ }
1523
+ const description = a.frontmatter.description || "";
1524
+ const out = `# Harness-Bujang \u2014 ${a.slug}
1525
+
1526
+ ` + (description ? `${description}
1527
+
1528
+ ` : "") + `> Source of truth: \`.claude/agents/${a.slug}.md\` \u2014 re-run \`bujang adapt --to=cline\` to sync.
1529
+
1530
+ ---
1531
+
1532
+ ` + a.body.trim() + `
1533
+ `;
1534
+ await fs6.writeFile(file, out);
1535
+ console.log(` ${c5.green("\u2713")} bujang-${a.slug}.md`);
1536
+ }
1537
+ console.log();
1538
+ }
1539
+ async function adaptAider(target, agents, overwrite) {
1540
+ console.log(c5.bold("\u{1F4C2} Aider \u2014 CONVENTIONS.md + .aider.conf.yml"));
1541
+ const conventionsPath = path6.join(target, "CONVENTIONS.md");
1542
+ const conventionsExisted = await exists5(conventionsPath);
1543
+ if (conventionsExisted && !overwrite) {
1544
+ console.log(` ${c5.yellow("\u26A0")} CONVENTIONS.md ${c5.dim("(exists, skipped \u2014 use --yes to overwrite)")}`);
1545
+ } else {
1546
+ let body = `# Project Conventions \u2014 Harness-Bujang
1547
+
1548
+ `;
1549
+ body += `> Source of truth: \`.claude/agents/*.md\` \u2014 re-run \`bujang adapt --to=aider\` to sync.
1550
+
1551
+ `;
1552
+ body += `This file collects the multi-agent harness role guides into a single conventions file that Aider can load via \`.aider.conf.yml\`. Aider does not natively dispatch to subagents, so when the user's request matches a specific role's domain, internally adopt that role's instructions for the response.
1553
+
1554
+ `;
1555
+ body += `## Roles
1556
+
1557
+ `;
1558
+ for (const a of agents) {
1559
+ const desc = a.frontmatter.description || "";
1560
+ body += `- **${a.slug}**${desc ? ` \u2014 ${desc.replace(/\n/g, " ").slice(0, 200)}` : ""}
1561
+ `;
1562
+ }
1563
+ body += `
1564
+ ---
1565
+
1566
+ `;
1567
+ for (const a of agents) {
1568
+ body += `## ${a.slug}
1569
+
1570
+ `;
1571
+ body += a.body.trim() + `
1572
+
1573
+ ---
1574
+
1575
+ `;
1576
+ }
1577
+ await fs6.writeFile(conventionsPath, body);
1578
+ console.log(` ${c5.green("\u2713")} CONVENTIONS.md ${c5.dim(`(${agents.length} roles concatenated)`)}`);
1579
+ }
1580
+ const aiderConfPath = path6.join(target, ".aider.conf.yml");
1581
+ const existing = await exists5(aiderConfPath) ? await fs6.readFile(aiderConfPath, "utf8") : "";
1582
+ if (existing.includes("CONVENTIONS.md")) {
1583
+ console.log(` ${c5.dim("\u2022")} .aider.conf.yml already references CONVENTIONS.md \u2014 left untouched`);
1584
+ } else if (existing && !overwrite) {
1585
+ console.log(` ${c5.yellow("\u26A0")} .aider.conf.yml exists and does NOT reference CONVENTIONS.md \u2014 skipped`);
1586
+ console.log(` ${c5.dim("Add manually:")} read: CONVENTIONS.md`);
1587
+ } else {
1588
+ const out = existing ? existing.trimEnd() + `
1589
+
1590
+ # Added by harness-bujang adapt
1591
+ read: CONVENTIONS.md
1592
+ ` : `# Aider config \u2014 auto-loads Harness-Bujang conventions
1593
+ read: CONVENTIONS.md
1594
+ `;
1595
+ await fs6.writeFile(aiderConfPath, out);
1596
+ console.log(` ${c5.green("\u2713")} .aider.conf.yml ${c5.dim("(read: CONVENTIONS.md)")}`);
1597
+ }
1598
+ console.log();
1599
+ }
1600
+ async function adaptCodex(target, agents, overwrite) {
1601
+ console.log(c5.bold("\u{1F4C2} Codex / Copilot Agent \u2014 AGENTS.md (project root)"));
1602
+ const filePath = path6.join(target, "AGENTS.md");
1603
+ if (await exists5(filePath) && !overwrite) {
1604
+ console.log(` ${c5.yellow("\u26A0")} AGENTS.md ${c5.dim("(exists, skipped \u2014 use --yes to overwrite)")}`);
1605
+ console.log();
1606
+ return;
1607
+ }
1608
+ let body = `# AGENTS.md \u2014 Harness-Bujang multi-agent harness
1609
+
1610
+ `;
1611
+ body += `> Source of truth: \`.claude/agents/*.md\` \u2014 re-run \`bujang adapt --to=codex\` to sync.
1612
+
1613
+ `;
1614
+ body += `This file follows the AGENTS.md convention adopted by OpenAI Codex CLI, GitHub Copilot Coding Agent, and several other agentic coding tools. It collects the harness role guides into a single document.
1615
+
1616
+ `;
1617
+ body += `When the user's request matches one of the role domains below, internally adopt that role's instructions for the response. If the request spans multiple domains, follow the **director** role's dispatch logic.
1618
+
1619
+ `;
1620
+ body += `## Roles
1621
+
1622
+ `;
1623
+ for (const a of agents) {
1624
+ const desc = a.frontmatter.description || "";
1625
+ body += `- **${a.slug}**${desc ? ` \u2014 ${desc.replace(/\n/g, " ").slice(0, 200)}` : ""}
1626
+ `;
1627
+ }
1628
+ body += `
1629
+ ---
1630
+
1631
+ `;
1632
+ for (const a of agents) {
1633
+ body += `## ${a.slug}
1634
+
1635
+ `;
1636
+ body += a.body.trim() + `
1637
+
1638
+ ---
1639
+
1640
+ `;
1641
+ }
1642
+ await fs6.writeFile(filePath, body);
1643
+ console.log(` ${c5.green("\u2713")} AGENTS.md ${c5.dim(`(${agents.length} roles concatenated, ${(body.length / 1024).toFixed(1)} KB)`)}`);
1644
+ console.log();
1645
+ }
1646
+ async function adaptGemini(target, agents, overwrite) {
1647
+ console.log(c5.bold("\u{1F4C2} Gemini / Antigravity \u2014 GEMINI.md + .gemini/styleguide.md"));
1648
+ const geminiMdPath = path6.join(target, "GEMINI.md");
1649
+ if (await exists5(geminiMdPath) && !overwrite) {
1650
+ console.log(` ${c5.yellow("\u26A0")} GEMINI.md ${c5.dim("(exists, skipped \u2014 use --yes to overwrite)")}`);
1651
+ } else {
1652
+ let body = `# GEMINI.md \u2014 Harness-Bujang multi-agent harness
1653
+
1654
+ `;
1655
+ body += `> Source of truth: \`.claude/agents/*.md\` \u2014 re-run \`bujang adapt --to=gemini\` to sync.
1656
+
1657
+ `;
1658
+ body += `This file is read by Google Antigravity (workspace highest priority), Gemini CLI, and Gemini Code Assist (workspace customization). It collects the harness role guides into a single document.
1659
+
1660
+ `;
1661
+ body += `When the user's request matches one of the role domains below, internally adopt that role's instructions for the response. If the request spans multiple domains, follow the **director** role's dispatch logic.
1662
+
1663
+ `;
1664
+ body += `## Roles
1665
+
1666
+ `;
1667
+ for (const a of agents) {
1668
+ const desc = a.frontmatter.description || "";
1669
+ body += `- **${a.slug}**${desc ? ` \u2014 ${desc.replace(/\n/g, " ").slice(0, 200)}` : ""}
1670
+ `;
1671
+ }
1672
+ body += `
1673
+ ---
1674
+
1675
+ `;
1676
+ for (const a of agents) {
1677
+ body += `## ${a.slug}
1678
+
1679
+ `;
1680
+ body += a.body.trim() + `
1681
+
1682
+ ---
1683
+
1684
+ `;
1685
+ }
1686
+ await fs6.writeFile(geminiMdPath, body);
1687
+ console.log(` ${c5.green("\u2713")} GEMINI.md ${c5.dim(`(${agents.length} roles concatenated, ${(body.length / 1024).toFixed(1)} KB)`)}`);
1688
+ }
1689
+ const styleguideDir = path6.join(target, ".gemini");
1690
+ await fs6.mkdir(styleguideDir, { recursive: true });
1691
+ const styleguidePath = path6.join(styleguideDir, "styleguide.md");
1692
+ if (await exists5(styleguidePath) && !overwrite) {
1693
+ console.log(` ${c5.yellow("\u26A0")} .gemini/styleguide.md ${c5.dim("(exists, skipped)")}`);
1694
+ } else {
1695
+ const reviewRoles = ["code-review-team", "security-team", "db-guard-team", "verifier-team"];
1696
+ const reviewAgents = agents.filter((a) => reviewRoles.includes(a.slug));
1697
+ let body = `# Code Review Style Guide \u2014 Harness-Bujang
1698
+
1699
+ `;
1700
+ body += `> Source of truth: \`.claude/agents/*.md\` \u2014 re-run \`bujang adapt --to=gemini\` to sync.
1701
+
1702
+ `;
1703
+ body += `This style guide is read by Gemini Code Assist for GitHub when reviewing PRs. It distills the review-relevant subset of the Harness-Bujang harness (code review, security, DB guard, verifier teams) into review criteria.
1704
+
1705
+ `;
1706
+ body += `When reviewing a PR, apply the following audit lenses in order:
1707
+
1708
+ `;
1709
+ for (const a of reviewAgents) {
1710
+ body += `## ${a.slug}
1711
+
1712
+ `;
1713
+ body += a.body.trim() + `
1714
+
1715
+ ---
1716
+
1717
+ `;
1718
+ }
1719
+ if (reviewAgents.length === 0) {
1720
+ body += `_(No review-team agents found in .claude/agents/. Re-run init to install the canonical set.)_
1721
+ `;
1722
+ }
1723
+ await fs6.writeFile(styleguidePath, body);
1724
+ console.log(` ${c5.green("\u2713")} .gemini/styleguide.md ${c5.dim(`(${reviewAgents.length} review roles)`)}`);
1725
+ }
1726
+ console.log();
1727
+ }
1728
+ async function loadAgents(agentsDir) {
1729
+ const entries = await fs6.readdir(agentsDir);
1730
+ const out = [];
1731
+ for (const name of entries) {
1732
+ if (!name.endsWith(".md")) continue;
1733
+ const src = path6.join(agentsDir, name);
1734
+ const raw = await fs6.readFile(src, "utf8");
1735
+ const slug = name.replace(/\.md$/, "");
1736
+ const { frontmatter, body } = splitFrontmatter(raw);
1737
+ out.push({ slug, frontmatter, body, src });
1738
+ }
1739
+ out.sort((a, b) => {
1740
+ if (a.slug === "director") return -1;
1741
+ if (b.slug === "director") return 1;
1742
+ return a.slug.localeCompare(b.slug);
1743
+ });
1744
+ return out;
1745
+ }
1746
+ function splitFrontmatter(raw) {
1747
+ if (!raw.startsWith("---\n")) {
1748
+ return { frontmatter: {}, body: raw };
1749
+ }
1750
+ const end = raw.indexOf("\n---\n", 4);
1751
+ if (end < 0) {
1752
+ return { frontmatter: {}, body: raw };
1753
+ }
1754
+ const fmRaw = raw.slice(4, end);
1755
+ const body = raw.slice(end + 5);
1756
+ const frontmatter = {};
1757
+ const lines = fmRaw.split(/\r?\n/);
1758
+ let currentKey = null;
1759
+ for (const line of lines) {
1760
+ const m = /^([a-zA-Z_-]+):\s?(.*)$/.exec(line);
1761
+ if (m && m[1] && !line.startsWith(" ") && !line.startsWith(" ")) {
1762
+ currentKey = m[1];
1763
+ frontmatter[currentKey] = m[2] ?? "";
1764
+ } else if (currentKey && (line.startsWith(" ") || line.startsWith(" "))) {
1765
+ frontmatter[currentKey] = (frontmatter[currentKey] ?? "") + " " + line.trim();
1766
+ }
1767
+ }
1768
+ return { frontmatter, body };
1769
+ }
1770
+ function escapeYamlString(s) {
1771
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1772
+ }
1773
+ function parseArgs4(args) {
1774
+ const targetRaw = getFlag4(args, "--target") ?? ".";
1775
+ const toRaw = getFlag4(args, "--to");
1776
+ if (!toRaw) {
1777
+ throw new Error(
1778
+ `--to=<cursor|cline|aider|codex|gemini|all> is required. Examples:
1779
+ bujang adapt --to=cursor
1780
+ bujang adapt --to=codex # AGENTS.md at project root
1781
+ bujang adapt --to=gemini # GEMINI.md + .gemini/styleguide.md
1782
+ bujang adapt --to=cursor,aider # multiple
1783
+ bujang adapt --to=all # cursor + cline + aider + codex + gemini`
1784
+ );
1785
+ }
1786
+ const targets = toRaw === "all" ? ["cursor", "cline", "aider", "codex", "gemini"] : toRaw.split(",").map((t) => t.trim());
1787
+ for (const t of targets) {
1788
+ if (!["cursor", "cline", "aider", "codex", "gemini"].includes(t)) {
1789
+ throw new Error(`Unknown adapter target "${t}" \u2014 expected one of: cursor, cline, aider, codex, gemini, all`);
1790
+ }
1791
+ }
1792
+ return {
1793
+ target: path6.resolve(targetRaw),
1794
+ to: targets,
1795
+ yes: args.includes("--yes") || args.includes("-y")
1796
+ };
1797
+ }
1798
+ function getFlag4(args, name) {
1799
+ for (const a of args) {
1800
+ if (a.startsWith(`${name}=`)) return a.slice(name.length + 1);
1801
+ }
1802
+ const idx = args.indexOf(name);
1803
+ if (idx >= 0 && idx + 1 < args.length && !args[idx + 1].startsWith("--")) {
1804
+ return args[idx + 1];
1805
+ }
1806
+ return void 0;
1807
+ }
1808
+ async function exists5(p) {
1809
+ try {
1810
+ await fs6.access(p);
1811
+ return true;
1812
+ } catch {
1813
+ return false;
1814
+ }
1815
+ }
1816
+
1817
+ // src/index.ts
1818
+ var c6 = {
1819
+ bold: (s) => `\x1B[1m${s}\x1B[22m`,
1820
+ dim: (s) => `\x1B[2m${s}\x1B[22m`,
1821
+ green: (s) => `\x1B[32m${s}\x1B[39m`,
1822
+ red: (s) => `\x1B[31m${s}\x1B[39m`,
1823
+ yellow: (s) => `\x1B[33m${s}\x1B[39m`,
1824
+ cyan: (s) => `\x1B[36m${s}\x1B[39m`
1825
+ };
1453
1826
  var HELP = `
1454
- ${c5.bold("harness-bujang")} \u2014 Korean-style multi-agent harness director for Claude Code
1455
- ${c5.dim("https://github.com/bjcho4141/harness-bujang")}
1827
+ ${c6.bold("harness-bujang")} \u2014 Korean-style multi-agent harness director for Claude Code
1828
+ ${c6.dim("https://github.com/bjcho4141/harness-bujang")}
1456
1829
 
1457
- ${c5.bold("Usage:")}
1458
- npx harness-bujang ${c5.cyan("init")} [options] Install the harness into a project
1459
- npx harness-bujang ${c5.cyan("status")} [options] Verify the harness install
1460
- npx harness-bujang ${c5.cyan("chat")} [options] Open the standalone chat-room viewer (any stack)
1461
- npx harness-bujang ${c5.cyan("migrate")} --to=<sqlite|supabase> Move chat data between backends
1830
+ ${c6.bold("Usage:")}
1831
+ npx harness-bujang ${c6.cyan("init")} [options] Install the harness into a project
1832
+ npx harness-bujang ${c6.cyan("status")} [options] Verify the harness install
1833
+ npx harness-bujang ${c6.cyan("chat")} [options] Open the standalone chat-room viewer (any stack)
1834
+ npx harness-bujang ${c6.cyan("adapt")} --to=<cursor|cline|aider|codex|gemini|all> Convert .claude/agents/ for other tools
1835
+ npx harness-bujang ${c6.cyan("migrate")} --to=<sqlite|supabase> Move chat data between backends
1462
1836
 
1463
- ${c5.bold("Options for init:")}
1464
- --lang=<ko|en> Agent language (default: en)
1837
+ ${c6.bold("Options for init:")}
1838
+ --lang=<ko|en> Agent language (default: ko \u2014 full \uBD80\uC7A5 persona)
1465
1839
  --chat=<sqlite|supabase> Chat-room backend (default: sqlite \u2014 local file, no setup)
1466
1840
  --commit-chat Don't gitignore .harness/ (for solo cross-machine sync via git)
1467
1841
  --target=<path> Project root (default: cwd)
@@ -1472,37 +1846,49 @@ ${c5.bold("Options for init:")}
1472
1846
  --no-learning-log Skip learning log seed
1473
1847
  --yes, -y Skip prompts and overwrite (non-interactive \u2014 for CI / scripts)
1474
1848
 
1475
- ${c5.dim("Run without --yes for an interactive setup (prompts for language, backend, etc.).")}
1849
+ ${c6.dim("Run without --yes for an interactive setup (prompts for language, backend, etc.).")}
1476
1850
 
1477
- ${c5.bold("Options for chat:")}
1851
+ ${c6.bold("Options for chat:")}
1478
1852
  --target=<path> Project root (default: cwd)
1479
1853
  --port=<number> Preferred port (default: 7777, falls forward if busy)
1480
1854
  --no-open Don't auto-open the browser
1481
1855
  --create Create an empty chat DB + schema if none exists yet
1482
1856
 
1483
- ${c5.bold("Options for migrate:")}
1857
+ ${c6.bold("Options for adapt:")}
1858
+ --to=<cursor|cline|aider|codex|gemini|all> Required \u2014 comma-separated list also OK
1859
+ --target=<path> Project root (default: cwd)
1860
+ --yes, -y Overwrite existing adapter files
1861
+
1862
+ ${c6.dim("Adapter targets:")}
1863
+ ${c6.dim(" cursor \u2192 .cursor/rules/bujang-*.mdc (Cursor IDE)")}
1864
+ ${c6.dim(" cline \u2192 .clinerules/bujang-*.md (Cline)")}
1865
+ ${c6.dim(" aider \u2192 CONVENTIONS.md + .aider.conf.yml (Aider)")}
1866
+ ${c6.dim(" codex \u2192 AGENTS.md (Codex CLI / Copilot Coding Agent / Cody)")}
1867
+ ${c6.dim(" gemini \u2192 GEMINI.md + .gemini/styleguide.md (Antigravity / Gemini CLI / Code Assist)")}
1868
+
1869
+ ${c6.bold("Options for migrate:")}
1484
1870
  --to=<sqlite|supabase> Required \u2014 target backend
1485
1871
  --target=<path> Project root (default: cwd)
1486
1872
  --yes, -y Skip confirmation
1487
1873
 
1488
- ${c5.bold("Examples:")}
1489
- ${c5.dim("# Install Korean Bujang persona, SQLite chat (default \u2014 zero setup)")}
1874
+ ${c6.bold("Examples:")}
1875
+ ${c6.dim("# Install Korean Bujang persona, SQLite chat (default \u2014 zero setup)")}
1490
1876
  npx harness-bujang init --lang=ko
1491
1877
 
1492
- ${c5.dim("# Open the standalone chat-room \u2014 works on ANY stack (Next.js, Rails, Django, \u2026)")}
1878
+ ${c6.dim("# Open the standalone chat-room \u2014 works on ANY stack (Next.js, Rails, Django, \u2026)")}
1493
1879
  npx harness-bujang chat
1494
- ${c5.dim("# \u2192 opens http://localhost:7777 in your browser")}
1880
+ ${c6.dim("# \u2192 opens http://localhost:7777 in your browser")}
1495
1881
 
1496
- ${c5.dim("# Solo, multiple machines \u2014 sync chat history via git")}
1882
+ ${c6.dim("# Solo, multiple machines \u2014 sync chat history via git")}
1497
1883
  npx harness-bujang init --commit-chat
1498
1884
 
1499
- ${c5.dim("# Production project with team sharing \u2014 Supabase backend")}
1885
+ ${c6.dim("# Production project with team sharing \u2014 Supabase backend")}
1500
1886
  npx harness-bujang init --chat=supabase
1501
1887
 
1502
- ${c5.dim("# Started solo, now scaling up \u2014 promote to cloud")}
1888
+ ${c6.dim("# Started solo, now scaling up \u2014 promote to cloud")}
1503
1889
  bujang migrate --to=supabase
1504
1890
 
1505
- ${c5.dim("# Going back to solo / archive \u2014 pull cloud data into local SQLite")}
1891
+ ${c6.dim("# Going back to solo / archive \u2014 pull cloud data into local SQLite")}
1506
1892
  bujang migrate --to=sqlite
1507
1893
  `;
1508
1894
  async function main() {
@@ -1518,12 +1904,15 @@ async function main() {
1518
1904
  case "chat":
1519
1905
  await runChat(args.slice(1));
1520
1906
  break;
1907
+ case "adapt":
1908
+ await runAdapt(args.slice(1));
1909
+ break;
1521
1910
  case "migrate":
1522
1911
  await runMigrate(args.slice(1));
1523
1912
  break;
1524
1913
  case "--version":
1525
1914
  case "-v":
1526
- console.log("0.3.0");
1915
+ console.log("0.4.2");
1527
1916
  break;
1528
1917
  case "--help":
1529
1918
  case "-h":
@@ -1531,13 +1920,13 @@ async function main() {
1531
1920
  console.log(HELP);
1532
1921
  break;
1533
1922
  default:
1534
- console.error(c5.red(`Unknown command: ${command}`));
1923
+ console.error(c6.red(`Unknown command: ${command}`));
1535
1924
  console.log(HELP);
1536
1925
  process.exit(1);
1537
1926
  }
1538
1927
  }
1539
1928
  main().catch((err) => {
1540
- console.error(c5.red(`
1929
+ console.error(c6.red(`
1541
1930
  \u2716 ${err.message}`));
1542
1931
  if (process.env.DEBUG) console.error(err.stack);
1543
1932
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-bujang",
3
- "version": "0.3.0",
3
+ "version": "0.4.2",
4
4
  "description": "Install the Harness-Bujang multi-agent harness into any project — Director, 7 specialist teams, real-time chat-room UI. Korean and English personas. Works with Claude Code, Cursor, Cline, Aider, or any tool that reads .claude/agents/.",
5
5
  "keywords": [
6
6
  "claude-code",