harness-bujang 0.4.2 → 0.5.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.
- package/README.md +59 -0
- package/dist/index.js +307 -57
- package/package.json +1 -1
- package/templates/agents/en/analysis-team.md +66 -0
- package/templates/agents/en/cofounder.md +100 -0
- package/templates/agents/en/content-qa-team.md +107 -0
- package/templates/agents/en/director.md +165 -2
- package/templates/agents/en/edit-team.md +107 -0
- package/templates/agents/en/image-team.md +87 -0
- package/templates/agents/en/research-team.md +63 -0
- package/templates/agents/en/script-team.md +75 -0
- package/templates/agents/en/voice-team.md +82 -0
- package/templates/agents/ko/analysis-team.md +68 -0
- package/templates/agents/ko/cofounder.md +100 -0
- package/templates/agents/ko/content-qa-team.md +108 -0
- package/templates/agents/ko/director.md +165 -2
- package/templates/agents/ko/edit-team.md +107 -0
- package/templates/agents/ko/image-team.md +87 -0
- package/templates/agents/ko/research-team.md +63 -0
- package/templates/agents/ko/script-team.md +76 -0
- package/templates/agents/ko/voice-team.md +93 -0
- package/templates/project-template/app/admin/harness/harness-client.tsx +38 -14
package/README.md
CHANGED
|
@@ -18,8 +18,24 @@ 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
|
|
21
|
+
|
|
22
|
+
# Upgrade an existing install — adds NEW team files only, never touches existing ones
|
|
23
|
+
npx harness-bujang update
|
|
21
24
|
```
|
|
22
25
|
|
|
26
|
+
> ## ⚠️ First-time vs upgrade — read this before re-running
|
|
27
|
+
>
|
|
28
|
+
> | Situation | Command | What it does |
|
|
29
|
+
> |-----------|---------|-------------|
|
|
30
|
+
> | **First install** (empty project) | `npx harness-bujang init` | Full install — director + 16 teams + cofounder |
|
|
31
|
+
> | **Pulling new version** (already installed) | `npx harness-bujang update` | **Adds only NEW files.** Existing agent files are never touched |
|
|
32
|
+
> | **Clean reset** (drop customizations) | `npx harness-bujang init --yes` | **Overwrites every agent file** ⚠️ |
|
|
33
|
+
>
|
|
34
|
+
> **If you've already been using harness-bujang, use `update`.** Running
|
|
35
|
+
> `init --yes` to upgrade will destroy any domain rules / learned customizations
|
|
36
|
+
> you've added to existing agent files. `CLAUDE.md` and
|
|
37
|
+
> `docs/AGENT_LEARNING_LOG.md` are never touched by any of the three commands.
|
|
38
|
+
|
|
23
39
|
### See the chat-room — any stack
|
|
24
40
|
|
|
25
41
|
```bash
|
|
@@ -72,6 +88,49 @@ npx harness-bujang status [path]
|
|
|
72
88
|
|
|
73
89
|
Verifies the install: agent files, `CLAUDE.md` section, learning log, chat-room UI. Counts unfilled `{{...}}` placeholders.
|
|
74
90
|
|
|
91
|
+
### `update`
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
npx harness-bujang update [options]
|
|
95
|
+
|
|
96
|
+
Options:
|
|
97
|
+
--target=<path> Project root (default: cwd)
|
|
98
|
+
--lang=<ko|en> Language for newly-added agents (default: ko)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Safe additive update.** Adds NEW agent files only. Existing files are NEVER touched.
|
|
102
|
+
|
|
103
|
+
Use this when you upgrade `harness-bujang` and want to pull in newly-introduced
|
|
104
|
+
team members (e.g. the cofounder persona in 0.5.1, or the content-production
|
|
105
|
+
teams in 0.5.0) without disturbing any local customizations.
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
$ npx harness-bujang update
|
|
109
|
+
|
|
110
|
+
🔄 Harness-Bujang update
|
|
111
|
+
📂 Checking .claude/agents/
|
|
112
|
+
= consultant.md (exists, kept as-is)
|
|
113
|
+
= dev-team.md (exists, kept as-is)
|
|
114
|
+
+ cofounder.md
|
|
115
|
+
+ image-team.md
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
📋 Summary
|
|
119
|
+
Added: 4 (new files only)
|
|
120
|
+
Kept: 14 (existing files untouched)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> ⚠️ **`update` vs `init --yes`** — pick the right one:
|
|
124
|
+
>
|
|
125
|
+
> | Command | What happens |
|
|
126
|
+
> |---------|-------------|
|
|
127
|
+
> | `npx harness-bujang update` | **Safe.** Adds only new files. Custom edits preserved. |
|
|
128
|
+
> | `npx harness-bujang init --yes` | **Destructive.** Overwrites every agent file. Customizations lost. |
|
|
129
|
+
>
|
|
130
|
+
> Default to `update` for upgrades. Use `init --yes` only for a clean reset.
|
|
131
|
+
|
|
132
|
+
`CLAUDE.md` and `docs/AGENT_LEARNING_LOG.md` are **never** touched by either command.
|
|
133
|
+
|
|
75
134
|
### `chat`
|
|
76
135
|
|
|
77
136
|
```
|
package/dist/index.js
CHANGED
|
@@ -862,11 +862,11 @@ async function upsertEnvVar(envFile, key, value) {
|
|
|
862
862
|
}
|
|
863
863
|
async function confirm2(message) {
|
|
864
864
|
process.stdout.write(`${message} [y/N] `);
|
|
865
|
-
return new Promise((
|
|
865
|
+
return new Promise((resolve7) => {
|
|
866
866
|
process.stdin.setEncoding("utf8");
|
|
867
867
|
process.stdin.once("data", (chunk) => {
|
|
868
868
|
const ans = chunk.toString().trim().toLowerCase();
|
|
869
|
-
|
|
869
|
+
resolve7(ans === "y" || ans === "yes");
|
|
870
870
|
process.stdin.pause();
|
|
871
871
|
});
|
|
872
872
|
});
|
|
@@ -1009,7 +1009,7 @@ async function runChat(args) {
|
|
|
1009
1009
|
res.writeHead(404);
|
|
1010
1010
|
res.end("not found");
|
|
1011
1011
|
});
|
|
1012
|
-
await new Promise((
|
|
1012
|
+
await new Promise((resolve7) => server.listen(port, "127.0.0.1", resolve7));
|
|
1013
1013
|
const url = `http://localhost:${port}`;
|
|
1014
1014
|
console.log();
|
|
1015
1015
|
console.log(c4.bold(c4.green("\u{1F7E2} \uD558\uB124\uC2A4 \uD1A1\uBC29 viewer")) + c4.dim(" \u2014 " + url));
|
|
@@ -1061,12 +1061,12 @@ async function findOpenPort(preferred) {
|
|
|
1061
1061
|
throw new Error(`Could not find a free port in range ${preferred}-${preferred + 19}`);
|
|
1062
1062
|
}
|
|
1063
1063
|
function portIsFree(port) {
|
|
1064
|
-
return new Promise((
|
|
1064
|
+
return new Promise((resolve7) => {
|
|
1065
1065
|
const tester = http.createServer();
|
|
1066
|
-
tester.once("error", () =>
|
|
1066
|
+
tester.once("error", () => resolve7(false));
|
|
1067
1067
|
tester.once("listening", () => {
|
|
1068
1068
|
tester.close();
|
|
1069
|
-
|
|
1069
|
+
resolve7(true);
|
|
1070
1070
|
});
|
|
1071
1071
|
tester.listen(port, "127.0.0.1");
|
|
1072
1072
|
});
|
|
@@ -1080,10 +1080,10 @@ function openBrowser(url) {
|
|
|
1080
1080
|
}
|
|
1081
1081
|
}
|
|
1082
1082
|
function readBody(req) {
|
|
1083
|
-
return new Promise((
|
|
1083
|
+
return new Promise((resolve7, reject) => {
|
|
1084
1084
|
const chunks = [];
|
|
1085
1085
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
1086
|
-
req.on("end", () =>
|
|
1086
|
+
req.on("end", () => resolve7(Buffer.concat(chunks).toString("utf8")));
|
|
1087
1087
|
req.on("error", reject);
|
|
1088
1088
|
});
|
|
1089
1089
|
}
|
|
@@ -1165,37 +1165,63 @@ var CLIENT_JS = (
|
|
|
1165
1165
|
/* js */
|
|
1166
1166
|
`
|
|
1167
1167
|
const ROLES = {
|
|
1168
|
-
'\uB300\uD45C\uB2D8':
|
|
1169
|
-
'\
|
|
1170
|
-
'
|
|
1171
|
-
'
|
|
1172
|
-
|
|
1173
|
-
'
|
|
1174
|
-
'
|
|
1175
|
-
'
|
|
1176
|
-
'
|
|
1177
|
-
'
|
|
1178
|
-
'
|
|
1168
|
+
'\uB300\uD45C\uB2D8': { icon: '\u{1F454}', color: 'text-purple-700', bg: 'bg-purple-100', label: '\uB300\uD45C\uB2D8' },
|
|
1169
|
+
'\uACF5\uB3D9\uB300\uD45C': { icon: '\u2B50', color: 'text-violet-700', bg: 'bg-violet-100', label: '\uACF5\uB3D9\uB300\uD45C' },
|
|
1170
|
+
'\uBD80\uC7A5': { icon: '\u{1F9D1}\u200D\u{1F4BC}', color: 'text-blue-700', bg: 'bg-blue-100', label: '\uBD80\uC7A5' },
|
|
1171
|
+
'\uC678\uBD80\uD300\uC6D0': { icon: '\u{1F310}', color: 'text-gray-700', bg: 'bg-gray-100', label: '\uC678\uBD80\uD300\uC6D0' },
|
|
1172
|
+
// Engineering core teams
|
|
1173
|
+
'consultant': { icon: '\u{1F91D}', color: 'text-indigo-700', bg: 'bg-indigo-100', label: '\uCEE8\uC124\uD134\uD2B8' },
|
|
1174
|
+
'dev-team': { icon: '\u{1F4BB}', color: 'text-violet-700', bg: 'bg-violet-100', label: '\uAC1C\uBC1C\uD300' },
|
|
1175
|
+
'architect-team': { icon: '\u{1F3D7}\uFE0F', color: 'text-cyan-700', bg: 'bg-cyan-100', label: '\uC544\uD0A4\uD14D\uCC98\uD300' },
|
|
1176
|
+
'code-review-team': { icon: '\u{1F4DD}', color: 'text-yellow-700', bg: 'bg-yellow-100', label: '\uCF54\uB4DC\uB9AC\uBDF0\uD300' },
|
|
1177
|
+
'doc-sync-team': { icon: '\u{1F4C4}', color: 'text-orange-700', bg: 'bg-orange-100', label: '\uBB38\uC11C\uAD00\uB9AC\uD300' },
|
|
1178
|
+
'security-team': { icon: '\u{1F6E1}\uFE0F', color: 'text-red-700', bg: 'bg-red-100', label: '\uBCF4\uC548\uD300' },
|
|
1179
|
+
'db-guard-team': { icon: '\u{1F5C4}\uFE0F', color: 'text-green-700', bg: 'bg-green-100', label: 'DB\uD300' },
|
|
1180
|
+
'qa-team': { icon: '\u{1F9EA}', color: 'text-teal-700', bg: 'bg-teal-100', label: 'QA\uD300' },
|
|
1181
|
+
'verifier-team': { icon: '\u2705', color: 'text-emerald-700', bg: 'bg-emerald-100', label: '\uAC80\uC218\uD300' },
|
|
1182
|
+
// Content production teams (added 0.5.0)
|
|
1183
|
+
'research-team': { icon: '\u{1F50D}', color: 'text-sky-700', bg: 'bg-sky-100', label: '\uB9AC\uC11C\uCE58\uD300' },
|
|
1184
|
+
'analysis-team': { icon: '\u{1F4CA}', color: 'text-amber-700', bg: 'bg-amber-100', label: '\uBD84\uC11D\uD300' },
|
|
1185
|
+
'script-team': { icon: '\u270D\uFE0F', color: 'text-pink-700', bg: 'bg-pink-100', label: '\uB300\uBCF8\uD300' },
|
|
1186
|
+
'image-team': { icon: '\u{1F3A8}', color: 'text-fuchsia-700',bg: 'bg-fuchsia-100',label: '\uC774\uBBF8\uC9C0\uD300' },
|
|
1187
|
+
'voice-team': { icon: '\u{1F399}\uFE0F', color: 'text-rose-700', bg: 'bg-rose-100', label: '\uC74C\uC131\uD300' },
|
|
1188
|
+
'edit-team': { icon: '\u{1F3AC}', color: 'text-stone-700', bg: 'bg-stone-100', label: '\uD3B8\uC9D1\uD300' },
|
|
1189
|
+
'content-qa-team': { icon: '\u{1F50E}', color: 'text-lime-700', bg: 'bg-lime-100', label: '\uCF58\uD150\uCE20\uAC80\uC218\uD300' },
|
|
1179
1190
|
};
|
|
1180
1191
|
|
|
1181
1192
|
const ROOMS = [
|
|
1182
|
-
|
|
1193
|
+
// Top-level
|
|
1194
|
+
{ id: '\uB300\uD45C\uB2D8', name: '\uB300\uD45C \uBCF4\uACE0', icon: '\u{1F454}', members: ['\uB300\uD45C\uB2D8', '\uACF5\uB3D9\uB300\uD45C', 'consultant', '\uBD80\uC7A5'] },
|
|
1195
|
+
{ id: '\uACF5\uB3D9\uB300\uD45C', name: '\uACF5\uB3D9\uB300\uD45C', icon: '\u2B50', members: ['\uB300\uD45C\uB2D8', '\uACF5\uB3D9\uB300\uD45C', '\uBD80\uC7A5'] },
|
|
1183
1196
|
{ id: 'consultant', name: '\uCEE8\uC124\uD134\uD2B8', icon: '\u{1F91D}', members: ['consultant', '\uBD80\uC7A5'] },
|
|
1197
|
+
// Engineering teams
|
|
1184
1198
|
{ id: 'architect-team', name: '\uC544\uD0A4\uD14D\uCC98\uD300', icon: '\u{1F3D7}\uFE0F', members: ['\uBD80\uC7A5', 'architect-team'] },
|
|
1199
|
+
{ id: 'dev-team', name: '\uAC1C\uBC1C\uD300', icon: '\u{1F4BB}', members: ['\uBD80\uC7A5', 'dev-team'] },
|
|
1185
1200
|
{ id: 'code-review-team', name: '\uCF54\uB4DC\uB9AC\uBDF0\uD300', icon: '\u{1F4DD}', members: ['\uBD80\uC7A5', 'code-review-team'] },
|
|
1186
|
-
{ id: 'doc-sync-team', name: '\uBB38\uC11C\uAD00\uB9AC\uD300', icon: '\u{1F4C4}', members: ['\uBD80\uC7A5', 'doc-sync-team'] },
|
|
1187
1201
|
{ id: 'security-team', name: '\uBCF4\uC548\uD300', icon: '\u{1F6E1}\uFE0F', members: ['\uBD80\uC7A5', 'security-team'] },
|
|
1188
1202
|
{ id: 'db-guard-team', name: 'DB\uD300', icon: '\u{1F5C4}\uFE0F', members: ['\uBD80\uC7A5', 'db-guard-team'] },
|
|
1189
1203
|
{ id: 'qa-team', name: 'QA\uD300', icon: '\u{1F9EA}', members: ['\uBD80\uC7A5', 'qa-team'] },
|
|
1190
1204
|
{ id: 'verifier-team', name: '\uAC80\uC218\uD300', icon: '\u2705', members: ['\uBD80\uC7A5', 'verifier-team'] },
|
|
1191
|
-
{ id: '
|
|
1205
|
+
{ id: 'doc-sync-team', name: '\uBB38\uC11C\uAD00\uB9AC\uD300', icon: '\u{1F4C4}', members: ['\uBD80\uC7A5', 'doc-sync-team'] },
|
|
1206
|
+
// Content production teams (added 0.5.0)
|
|
1207
|
+
{ id: 'research-team', name: '\uB9AC\uC11C\uCE58\uD300', icon: '\u{1F50D}', members: ['\uBD80\uC7A5', 'research-team'] },
|
|
1208
|
+
{ id: 'analysis-team', name: '\uBD84\uC11D\uD300', icon: '\u{1F4CA}', members: ['\uBD80\uC7A5', 'analysis-team'] },
|
|
1209
|
+
{ id: 'script-team', name: '\uB300\uBCF8\uD300', icon: '\u270D\uFE0F', members: ['\uBD80\uC7A5', 'script-team'] },
|
|
1210
|
+
{ id: 'image-team', name: '\uC774\uBBF8\uC9C0\uD300', icon: '\u{1F3A8}', members: ['\uBD80\uC7A5', 'image-team'] },
|
|
1211
|
+
{ id: 'voice-team', name: '\uC74C\uC131\uD300', icon: '\u{1F399}\uFE0F', members: ['\uBD80\uC7A5', 'voice-team'] },
|
|
1212
|
+
{ id: 'edit-team', name: '\uD3B8\uC9D1\uD300', icon: '\u{1F3AC}', members: ['\uBD80\uC7A5', 'edit-team'] },
|
|
1213
|
+
{ id: 'content-qa-team', name: '\uCF58\uD150\uCE20\uAC80\uC218\uD300', icon: '\u{1F50E}', members: ['\uBD80\uC7A5', 'content-qa-team'] },
|
|
1214
|
+
// External (0.5.1) \u2014 catches any from/to == '\uC678\uBD80\uD300\uC6D0' (Director's external dispatch logging)
|
|
1215
|
+
{ id: '\uC678\uBD80\uD300\uC6D0', name: '\uC678\uBD80\uD300\uC6D0', icon: '\u{1F310}', members: ['\uBD80\uC7A5', '\uC678\uBD80\uD300\uC6D0', '\uACF5\uB3D9\uB300\uD45C'] },
|
|
1192
1216
|
];
|
|
1193
1217
|
|
|
1194
1218
|
const STORAGE_KEY = 'harness-bujang-read';
|
|
1219
|
+
const FILTER_KEY = 'harness-bujang-filter';
|
|
1195
1220
|
const state = {
|
|
1196
1221
|
messages: [],
|
|
1197
1222
|
selectedRoom: null,
|
|
1198
1223
|
readCounts: JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'),
|
|
1224
|
+
filter: localStorage.getItem(FILTER_KEY) || 'all', // 'all' | 'unread'
|
|
1199
1225
|
loading: true,
|
|
1200
1226
|
};
|
|
1201
1227
|
|
|
@@ -1272,6 +1298,16 @@ function render() {
|
|
|
1272
1298
|
const warnings = state.messages.filter((m) => m.severity === 'warning').length;
|
|
1273
1299
|
const infos = state.messages.filter((m) => m.severity === 'info').length;
|
|
1274
1300
|
|
|
1301
|
+
// Pre-compute unread per room (for the filter button + badges).
|
|
1302
|
+
const unreadByRoom = {};
|
|
1303
|
+
let totalUnread = 0;
|
|
1304
|
+
for (const room of ROOMS) {
|
|
1305
|
+
const count = filterMessages(state.messages, room.id).length;
|
|
1306
|
+
const unread = Math.max(0, count - (state.readCounts[room.id] || 0));
|
|
1307
|
+
unreadByRoom[room.id] = unread;
|
|
1308
|
+
totalUnread += unread;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1275
1311
|
let html = '<div class="w-80 border-r border-gray-200 bg-white flex flex-col h-full">';
|
|
1276
1312
|
html += '<div class="p-4 border-b border-gray-200">';
|
|
1277
1313
|
html += '<h1 class="text-lg font-bold text-gray-900">\uD558\uB124\uC2A4 \uD1A1\uBC29</h1>';
|
|
@@ -1283,14 +1319,38 @@ function render() {
|
|
|
1283
1319
|
if (infos) html += '<span class="px-2 py-0.5 text-xs font-bold bg-green-100 text-green-700 rounded-full">INFO ' + infos + '</span>';
|
|
1284
1320
|
html += '</div>';
|
|
1285
1321
|
}
|
|
1322
|
+
|
|
1323
|
+
// Filter buttons \u2014 KakaoTalk-style: \uC804\uCCB4 / \uC548\uC77D\uC74C
|
|
1324
|
+
html += '<div class="flex gap-2 mt-3">';
|
|
1325
|
+
html += '<button data-filter="all" class="px-3 py-1.5 text-xs font-semibold rounded-full border transition-colors ' +
|
|
1326
|
+
(state.filter === 'all' ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50') +
|
|
1327
|
+
'">\uC804\uCCB4</button>';
|
|
1328
|
+
html += '<button data-filter="unread" class="px-3 py-1.5 text-xs font-semibold rounded-full border transition-colors flex items-center gap-1.5 ' +
|
|
1329
|
+
(state.filter === 'unread' ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50') +
|
|
1330
|
+
'">';
|
|
1331
|
+
html += '<span>\u{1F4AC} \uC548\uC77D\uC74C</span>';
|
|
1332
|
+
if (totalUnread > 0) {
|
|
1333
|
+
html += '<span class="px-1.5 py-0.5 text-[10px] font-bold bg-red-500 text-white rounded-full">' + totalUnread + '</span>';
|
|
1334
|
+
}
|
|
1335
|
+
html += '</button>';
|
|
1336
|
+
html += '</div>';
|
|
1286
1337
|
html += '</div>';
|
|
1287
1338
|
|
|
1288
1339
|
html += '<div class="flex-1 overflow-y-auto">';
|
|
1289
|
-
|
|
1340
|
+
// When 'unread' filter is active, only show rooms with unread > 0.
|
|
1341
|
+
const visibleRooms = state.filter === 'unread'
|
|
1342
|
+
? ROOMS.filter((r) => unreadByRoom[r.id] > 0)
|
|
1343
|
+
: ROOMS;
|
|
1344
|
+
|
|
1345
|
+
if (visibleRooms.length === 0) {
|
|
1346
|
+
html += '<div class="px-4 py-12 text-center"><p class="text-3xl mb-2">\u{1F4ED}</p><p class="text-xs text-gray-500">\uC548\uC77D\uC740 \uD1A1\uBC29\uC774 \uC5C6\uC2B5\uB2C8\uB2E4</p></div>';
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
for (const room of visibleRooms) {
|
|
1290
1350
|
const last = getLastMessage(state.messages, room.id);
|
|
1291
1351
|
const count = filterMessages(state.messages, room.id).length;
|
|
1292
1352
|
const isSelected = state.selectedRoom === room.id;
|
|
1293
|
-
const unread =
|
|
1353
|
+
const unread = unreadByRoom[room.id];
|
|
1294
1354
|
html += '<button data-room-id="' + escapeHtml(room.id) + '" class="w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ' +
|
|
1295
1355
|
(isSelected ? 'bg-indigo-50' : 'hover:bg-gray-50') + '">';
|
|
1296
1356
|
html += '<div class="flex-shrink-0 w-12 h-12 rounded-2xl bg-gray-100 flex items-center justify-center text-2xl">' + room.icon + '</div>';
|
|
@@ -1365,6 +1425,15 @@ function render() {
|
|
|
1365
1425
|
|
|
1366
1426
|
root.innerHTML = html;
|
|
1367
1427
|
|
|
1428
|
+
// Re-bind filter buttons (\uC804\uCCB4 / \uC548\uC77D\uC74C).
|
|
1429
|
+
document.querySelectorAll('[data-filter]').forEach((el) => {
|
|
1430
|
+
el.addEventListener('click', () => {
|
|
1431
|
+
state.filter = el.getAttribute('data-filter');
|
|
1432
|
+
localStorage.setItem(FILTER_KEY, state.filter);
|
|
1433
|
+
render();
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1368
1437
|
// Re-bind room-click handlers (room list).
|
|
1369
1438
|
document.querySelectorAll('[data-room-id]').forEach((el) => {
|
|
1370
1439
|
el.addEventListener('click', () => {
|
|
@@ -1814,7 +1883,12 @@ async function exists5(p) {
|
|
|
1814
1883
|
}
|
|
1815
1884
|
}
|
|
1816
1885
|
|
|
1817
|
-
// src/
|
|
1886
|
+
// src/update.ts
|
|
1887
|
+
import * as fs7 from "fs/promises";
|
|
1888
|
+
import * as path7 from "path";
|
|
1889
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1890
|
+
var __filename3 = fileURLToPath2(import.meta.url);
|
|
1891
|
+
var __dirname3 = path7.dirname(__filename3);
|
|
1818
1892
|
var c6 = {
|
|
1819
1893
|
bold: (s) => `\x1B[1m${s}\x1B[22m`,
|
|
1820
1894
|
dim: (s) => `\x1B[2m${s}\x1B[22m`,
|
|
@@ -1823,18 +1897,184 @@ var c6 = {
|
|
|
1823
1897
|
yellow: (s) => `\x1B[33m${s}\x1B[39m`,
|
|
1824
1898
|
cyan: (s) => `\x1B[36m${s}\x1B[39m`
|
|
1825
1899
|
};
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1900
|
+
async function runUpdate(args) {
|
|
1901
|
+
const opts = parseArgs5(args);
|
|
1902
|
+
const assets = await resolveAssetPaths2();
|
|
1903
|
+
const agentsSrc = path7.join(assets.agents, opts.lang);
|
|
1904
|
+
const agentsDst = path7.join(opts.target, ".claude/agents");
|
|
1905
|
+
console.log();
|
|
1906
|
+
console.log(c6.bold("\u{1F504} Harness-Bujang update"));
|
|
1907
|
+
console.log(c6.dim(` Target: ${opts.target}`));
|
|
1908
|
+
console.log(c6.dim(` Language: ${opts.lang}`));
|
|
1909
|
+
console.log();
|
|
1910
|
+
if (!await exists6(agentsDst)) {
|
|
1911
|
+
console.log(c6.yellow("\u26A0 No .claude/agents/ directory found."));
|
|
1912
|
+
console.log();
|
|
1913
|
+
console.log(" This project has not been initialized yet. Run:");
|
|
1914
|
+
console.log(` ${c6.cyan("npx harness-bujang init")}`);
|
|
1915
|
+
console.log();
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
const scan = await scanProject(opts.target);
|
|
1919
|
+
const context = await buildContext(opts, scan);
|
|
1920
|
+
const agentFiles = (await fs7.readdir(agentsSrc)).filter((f) => f.endsWith(".md"));
|
|
1921
|
+
agentFiles.sort();
|
|
1922
|
+
const added = [];
|
|
1923
|
+
const kept = [];
|
|
1924
|
+
console.log(c6.bold("\u{1F4C2} Checking .claude/agents/"));
|
|
1925
|
+
for (const f of agentFiles) {
|
|
1926
|
+
const dst = path7.join(agentsDst, f);
|
|
1927
|
+
if (await exists6(dst)) {
|
|
1928
|
+
kept.push(f);
|
|
1929
|
+
console.log(` ${c6.dim("=")} ${f} ${c6.dim("(exists, kept as-is)")}`);
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
const raw = await fs7.readFile(path7.join(agentsSrc, f), "utf8");
|
|
1933
|
+
await fs7.writeFile(dst, renderTemplate(raw, context));
|
|
1934
|
+
added.push(f);
|
|
1935
|
+
console.log(` ${c6.green("+")} ${f}`);
|
|
1936
|
+
}
|
|
1937
|
+
console.log();
|
|
1938
|
+
console.log(c6.bold("\u{1F4CB} Summary"));
|
|
1939
|
+
console.log(` ${c6.green("Added")}: ${added.length} ${added.length ? c6.dim("(new files only)") : ""}`);
|
|
1940
|
+
console.log(` ${c6.dim("Kept")}: ${kept.length} ${c6.dim("(existing files untouched)")}`);
|
|
1941
|
+
console.log();
|
|
1942
|
+
if (added.length === 0) {
|
|
1943
|
+
console.log(c6.dim(" Nothing new to install \u2014 your harness is already up to date."));
|
|
1944
|
+
console.log();
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
console.log(c6.bold(c6.green("\u2705 Update done.")));
|
|
1948
|
+
console.log();
|
|
1949
|
+
console.log(" Existing agent files were not modified. Your customizations are safe.");
|
|
1950
|
+
console.log(" To overwrite everything (e.g. for a clean reset), use instead:");
|
|
1951
|
+
console.log(` ${c6.cyan("npx harness-bujang init --yes")}`);
|
|
1952
|
+
console.log();
|
|
1953
|
+
}
|
|
1954
|
+
async function resolveAssetPaths2() {
|
|
1955
|
+
const packaged = path7.resolve(__dirname3, "..", "templates");
|
|
1956
|
+
if (await exists6(packaged)) {
|
|
1957
|
+
return {
|
|
1958
|
+
agents: path7.join(packaged, "agents"),
|
|
1959
|
+
templates: path7.join(packaged, "templates"),
|
|
1960
|
+
projectTemplate: path7.join(packaged, "project-template"),
|
|
1961
|
+
mode: "packaged"
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
const monorepoRoot = path7.resolve(__dirname3, "../../..");
|
|
1965
|
+
const sharedDir = path7.join(monorepoRoot, "shared");
|
|
1966
|
+
if (await exists6(sharedDir)) {
|
|
1967
|
+
return {
|
|
1968
|
+
agents: path7.join(sharedDir, "agents"),
|
|
1969
|
+
templates: path7.join(sharedDir, "templates"),
|
|
1970
|
+
projectTemplate: path7.join(monorepoRoot, "packages/template"),
|
|
1971
|
+
mode: "monorepo"
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
throw new Error(
|
|
1975
|
+
`Could not locate harness-bujang assets. Tried:
|
|
1976
|
+
- ${packaged}
|
|
1977
|
+
- ${sharedDir}
|
|
1978
|
+
`
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
async function buildContext(opts, scan) {
|
|
1982
|
+
return {
|
|
1983
|
+
PROJECT_PATH: opts.target,
|
|
1984
|
+
PROJECT_NAME: path7.basename(opts.target),
|
|
1985
|
+
PROJECT_CATEGORY: scan.framework.startsWith("Next.js") ? "Web application" : "Software project",
|
|
1986
|
+
DIFFERENTIATION: "(define your project differentiation here if relevant)",
|
|
1987
|
+
STACK_FRAMEWORK: scan.framework,
|
|
1988
|
+
STACK_LANGUAGE: scan.language,
|
|
1989
|
+
STACK_DB: scan.db,
|
|
1990
|
+
STACK_UI: scan.ui,
|
|
1991
|
+
STACK_PAYMENT: scan.payment,
|
|
1992
|
+
STACK_EXTRA: "(none)",
|
|
1993
|
+
HARNESS_TABLE: "harness_messages",
|
|
1994
|
+
ADMIN_HARNESS_ROUTE: "/admin/harness",
|
|
1995
|
+
LEARNING_LOG_PATH: "docs/AGENT_LEARNING_LOG.md",
|
|
1996
|
+
TASKS_TRACKER_GLOB: "docs/TASKS_*.md",
|
|
1997
|
+
BENCHMARK_DOC_PATH: "docs/BENCHMARK.md",
|
|
1998
|
+
GH_USER: scan.ghUser,
|
|
1999
|
+
BUILD_CMD: scan.buildCmd || "(no build script)",
|
|
2000
|
+
TYPECHECK_CMD: scan.typecheckCmd || "(no type-check command)",
|
|
2001
|
+
TEST_CMD: scan.testCmd || "(no tests configured)",
|
|
2002
|
+
E2E_CMD: scan.e2eCmd || "(no E2E setup)",
|
|
2003
|
+
DEV_URL: "http://localhost:3000",
|
|
2004
|
+
DB_TYPES_PATH: scan.dbTypesPath,
|
|
2005
|
+
DB_CLIENT_PATTERN: `Use the project's existing DB client convention. See ${scan.dbTypesPath}.`,
|
|
2006
|
+
KNOWN_SCHEMA_DRIFT: "(none documented yet)",
|
|
2007
|
+
COMMON_FK_HINTS: "(extract from your schema as you go)",
|
|
2008
|
+
ACCESS_POLICY_NOTES: "(document RLS / middleware as encountered)",
|
|
2009
|
+
MIGRATION_NAMING: "supabase/migrations/XXXXX_name.sql (or per-stack)",
|
|
2010
|
+
MIGRATION_APPLY_CMD: "supabase db push (or stack-specific)",
|
|
2011
|
+
ROUTE_GROUPS: scan.routeGroups,
|
|
2012
|
+
MIDDLEWARE_PATH: scan.middlewarePath,
|
|
2013
|
+
KEY_RELATIONSHIPS: "(document key entity relations as you go)",
|
|
2014
|
+
AUTH_GUARD_PATTERN: "(stack-specific)",
|
|
2015
|
+
ADMIN_GUARD_PATTERN: "(stack-specific)",
|
|
2016
|
+
API_RESPONSE_SHAPE: "{ data, error, message }",
|
|
2017
|
+
PRIMARY_COLOR: "#6366F1",
|
|
2018
|
+
FRAMEWORK_REVIEW_RULES: "",
|
|
2019
|
+
TEST_ACCOUNTS: "(define your test accounts here)",
|
|
2020
|
+
LEGAL_CONTEXT: "(no special legal context)",
|
|
2021
|
+
LANG_CODE: opts.lang,
|
|
2022
|
+
TODAY: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
2023
|
+
COMPLETED_DOCS_PATTERN: "docs/\uC644\uB8CC_*.md"
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
function parseArgs5(args) {
|
|
2027
|
+
const lang = getFlag5(args, "--lang") ?? "ko";
|
|
2028
|
+
if (!["ko", "en"].includes(lang)) {
|
|
2029
|
+
throw new Error(`--lang must be "ko" or "en", got "${lang}"`);
|
|
2030
|
+
}
|
|
2031
|
+
const targetRaw = getFlag5(args, "--target") ?? ".";
|
|
2032
|
+
return {
|
|
2033
|
+
target: path7.resolve(targetRaw),
|
|
2034
|
+
lang
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
function getFlag5(args, name) {
|
|
2038
|
+
for (const a of args) {
|
|
2039
|
+
if (a.startsWith(`${name}=`)) return a.slice(name.length + 1);
|
|
2040
|
+
}
|
|
2041
|
+
const idx = args.indexOf(name);
|
|
2042
|
+
if (idx >= 0 && idx + 1 < args.length && !args[idx + 1].startsWith("--")) {
|
|
2043
|
+
return args[idx + 1];
|
|
2044
|
+
}
|
|
2045
|
+
return void 0;
|
|
2046
|
+
}
|
|
2047
|
+
async function exists6(p) {
|
|
2048
|
+
try {
|
|
2049
|
+
await fs7.access(p);
|
|
2050
|
+
return true;
|
|
2051
|
+
} catch {
|
|
2052
|
+
return false;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
1836
2055
|
|
|
1837
|
-
|
|
2056
|
+
// src/index.ts
|
|
2057
|
+
var c7 = {
|
|
2058
|
+
bold: (s) => `\x1B[1m${s}\x1B[22m`,
|
|
2059
|
+
dim: (s) => `\x1B[2m${s}\x1B[22m`,
|
|
2060
|
+
green: (s) => `\x1B[32m${s}\x1B[39m`,
|
|
2061
|
+
red: (s) => `\x1B[31m${s}\x1B[39m`,
|
|
2062
|
+
yellow: (s) => `\x1B[33m${s}\x1B[39m`,
|
|
2063
|
+
cyan: (s) => `\x1B[36m${s}\x1B[39m`
|
|
2064
|
+
};
|
|
2065
|
+
var HELP = `
|
|
2066
|
+
${c7.bold("harness-bujang")} \u2014 Korean-style multi-agent harness director for Claude Code
|
|
2067
|
+
${c7.dim("https://github.com/bjcho4141/harness-bujang")}
|
|
2068
|
+
|
|
2069
|
+
${c7.bold("Usage:")}
|
|
2070
|
+
npx harness-bujang ${c7.cyan("init")} [options] Install the harness into a project
|
|
2071
|
+
npx harness-bujang ${c7.cyan("update")} [options] Pull NEW agents only \u2014 existing files untouched
|
|
2072
|
+
npx harness-bujang ${c7.cyan("status")} [options] Verify the harness install
|
|
2073
|
+
npx harness-bujang ${c7.cyan("chat")} [options] Open the standalone chat-room viewer (any stack)
|
|
2074
|
+
npx harness-bujang ${c7.cyan("adapt")} --to=<cursor|cline|aider|codex|gemini|all> Convert .claude/agents/ for other tools
|
|
2075
|
+
npx harness-bujang ${c7.cyan("migrate")} --to=<sqlite|supabase> Move chat data between backends
|
|
2076
|
+
|
|
2077
|
+
${c7.bold("Options for init:")}
|
|
1838
2078
|
--lang=<ko|en> Agent language (default: ko \u2014 full \uBD80\uC7A5 persona)
|
|
1839
2079
|
--chat=<sqlite|supabase> Chat-room backend (default: sqlite \u2014 local file, no setup)
|
|
1840
2080
|
--commit-chat Don't gitignore .harness/ (for solo cross-machine sync via git)
|
|
@@ -1846,49 +2086,56 @@ ${c6.bold("Options for init:")}
|
|
|
1846
2086
|
--no-learning-log Skip learning log seed
|
|
1847
2087
|
--yes, -y Skip prompts and overwrite (non-interactive \u2014 for CI / scripts)
|
|
1848
2088
|
|
|
1849
|
-
${
|
|
2089
|
+
${c7.dim("Run without --yes for an interactive setup (prompts for language, backend, etc.).")}
|
|
1850
2090
|
|
|
1851
|
-
${
|
|
2091
|
+
${c7.bold("Options for chat:")}
|
|
1852
2092
|
--target=<path> Project root (default: cwd)
|
|
1853
2093
|
--port=<number> Preferred port (default: 7777, falls forward if busy)
|
|
1854
2094
|
--no-open Don't auto-open the browser
|
|
1855
2095
|
--create Create an empty chat DB + schema if none exists yet
|
|
1856
2096
|
|
|
1857
|
-
${
|
|
2097
|
+
${c7.bold("Options for adapt:")}
|
|
1858
2098
|
--to=<cursor|cline|aider|codex|gemini|all> Required \u2014 comma-separated list also OK
|
|
1859
2099
|
--target=<path> Project root (default: cwd)
|
|
1860
2100
|
--yes, -y Overwrite existing adapter files
|
|
1861
2101
|
|
|
1862
|
-
${
|
|
1863
|
-
|
|
1864
|
-
|
|
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)")}
|
|
2102
|
+
${c7.bold("Options for update:")}
|
|
2103
|
+
--target=<path> Project root (default: cwd)
|
|
2104
|
+
--lang=<ko|en> Language for newly-added agents (default: ko)
|
|
1868
2105
|
|
|
1869
|
-
${
|
|
2106
|
+
${c7.dim(" update only adds NEW agent files. Existing files are NEVER touched.")}
|
|
2107
|
+
${c7.dim(" For a clean overwrite (resets all agents), use: bujang init --yes")}
|
|
2108
|
+
|
|
2109
|
+
${c7.dim("Adapter targets:")}
|
|
2110
|
+
${c7.dim(" cursor \u2192 .cursor/rules/bujang-*.mdc (Cursor IDE)")}
|
|
2111
|
+
${c7.dim(" cline \u2192 .clinerules/bujang-*.md (Cline)")}
|
|
2112
|
+
${c7.dim(" aider \u2192 CONVENTIONS.md + .aider.conf.yml (Aider)")}
|
|
2113
|
+
${c7.dim(" codex \u2192 AGENTS.md (Codex CLI / Copilot Coding Agent / Cody)")}
|
|
2114
|
+
${c7.dim(" gemini \u2192 GEMINI.md + .gemini/styleguide.md (Antigravity / Gemini CLI / Code Assist)")}
|
|
2115
|
+
|
|
2116
|
+
${c7.bold("Options for migrate:")}
|
|
1870
2117
|
--to=<sqlite|supabase> Required \u2014 target backend
|
|
1871
2118
|
--target=<path> Project root (default: cwd)
|
|
1872
2119
|
--yes, -y Skip confirmation
|
|
1873
2120
|
|
|
1874
|
-
${
|
|
1875
|
-
${
|
|
2121
|
+
${c7.bold("Examples:")}
|
|
2122
|
+
${c7.dim("# Install Korean Bujang persona, SQLite chat (default \u2014 zero setup)")}
|
|
1876
2123
|
npx harness-bujang init --lang=ko
|
|
1877
2124
|
|
|
1878
|
-
${
|
|
2125
|
+
${c7.dim("# Open the standalone chat-room \u2014 works on ANY stack (Next.js, Rails, Django, \u2026)")}
|
|
1879
2126
|
npx harness-bujang chat
|
|
1880
|
-
${
|
|
2127
|
+
${c7.dim("# \u2192 opens http://localhost:7777 in your browser")}
|
|
1881
2128
|
|
|
1882
|
-
${
|
|
2129
|
+
${c7.dim("# Solo, multiple machines \u2014 sync chat history via git")}
|
|
1883
2130
|
npx harness-bujang init --commit-chat
|
|
1884
2131
|
|
|
1885
|
-
${
|
|
2132
|
+
${c7.dim("# Production project with team sharing \u2014 Supabase backend")}
|
|
1886
2133
|
npx harness-bujang init --chat=supabase
|
|
1887
2134
|
|
|
1888
|
-
${
|
|
2135
|
+
${c7.dim("# Started solo, now scaling up \u2014 promote to cloud")}
|
|
1889
2136
|
bujang migrate --to=supabase
|
|
1890
2137
|
|
|
1891
|
-
${
|
|
2138
|
+
${c7.dim("# Going back to solo / archive \u2014 pull cloud data into local SQLite")}
|
|
1892
2139
|
bujang migrate --to=sqlite
|
|
1893
2140
|
`;
|
|
1894
2141
|
async function main() {
|
|
@@ -1907,12 +2154,15 @@ async function main() {
|
|
|
1907
2154
|
case "adapt":
|
|
1908
2155
|
await runAdapt(args.slice(1));
|
|
1909
2156
|
break;
|
|
2157
|
+
case "update":
|
|
2158
|
+
await runUpdate(args.slice(1));
|
|
2159
|
+
break;
|
|
1910
2160
|
case "migrate":
|
|
1911
2161
|
await runMigrate(args.slice(1));
|
|
1912
2162
|
break;
|
|
1913
2163
|
case "--version":
|
|
1914
2164
|
case "-v":
|
|
1915
|
-
console.log("0.
|
|
2165
|
+
console.log("0.5.2");
|
|
1916
2166
|
break;
|
|
1917
2167
|
case "--help":
|
|
1918
2168
|
case "-h":
|
|
@@ -1920,13 +2170,13 @@ async function main() {
|
|
|
1920
2170
|
console.log(HELP);
|
|
1921
2171
|
break;
|
|
1922
2172
|
default:
|
|
1923
|
-
console.error(
|
|
2173
|
+
console.error(c7.red(`Unknown command: ${command}`));
|
|
1924
2174
|
console.log(HELP);
|
|
1925
2175
|
process.exit(1);
|
|
1926
2176
|
}
|
|
1927
2177
|
}
|
|
1928
2178
|
main().catch((err) => {
|
|
1929
|
-
console.error(
|
|
2179
|
+
console.error(c7.red(`
|
|
1930
2180
|
\u2716 ${err.message}`));
|
|
1931
2181
|
if (process.env.DEBUG) console.error(err.stack);
|
|
1932
2182
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-bujang",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|