@walwal-harness/cli 6.1.4 → 6.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +43 -74
- package/assets/launchd/com.walwal.harness-wake.plist.template +32 -0
- package/assets/templates/CONVENTIONS.md +12 -0
- package/assets/templates/HARNESS.md +8 -8
- package/assets/templates/company-pipeline-manifest.json +71 -0
- package/assets/templates/config.json +29 -27
- package/assets/templates/memory.md +16 -2
- package/assets/templates/progress.json.template +6 -5
- package/bin/init.js +180 -90
- package/conventions/shared.md +34 -0
- package/gotchas/conductor.md +16 -0
- package/gotchas/dispatcher.md +16 -0
- package/gotchas/meeting-manager.md +8 -0
- package/gotchas/service-ops.md +8 -0
- package/package.json +4 -4
- package/scripts/conductor-tick.sh +157 -56
- package/scripts/harness-archive.sh +7 -5
- package/scripts/harness-dashboard-up.sh +11 -3
- package/scripts/harness-hourly-review.sh +582 -0
- package/scripts/harness-meeting-doc.sh +55 -16
- package/scripts/harness-next.sh +1 -1
- package/scripts/harness-parity-audit.sh +101 -0
- package/scripts/harness-queue-manager.sh +13 -6
- package/scripts/harness-service-ops-monitor.sh +273 -0
- package/scripts/harness-session-start.sh +60 -31
- package/scripts/harness-statusline.sh +8 -12
- package/scripts/harness-stop.sh +85 -0
- package/scripts/harness-user-prompt-submit.sh +11 -29
- package/scripts/harness-wake-install.sh +180 -0
- package/scripts/harness-wake.sh +350 -0
- package/scripts/harness-worker-dispatch.sh +188 -0
- package/scripts/lib/harness-agent-resolver.sh +127 -0
- package/scripts/lib/harness-progress-migrate.sh +6 -1
- package/scripts/lib/harness-render-progress.sh +3 -3
- package/skills/brainstorming/SKILL.md +2 -2
- package/skills/conductor/SKILL.md +67 -55
- package/skills/cqo/SKILL.md +1 -0
- package/skills/cto/SKILL.md +1 -0
- package/skills/dispatcher/SKILL.md +33 -14
- package/skills/evaluator-code-quality/SKILL.md +4 -4
- package/skills/evaluator-functional/SKILL.md +6 -6
- package/skills/evaluator-visual/SKILL.md +4 -4
- package/skills/generator-backend/SKILL.md +4 -4
- package/skills/generator-frontend/SKILL.md +5 -5
- package/skills/meeting-manager/SKILL.md +82 -12
- package/skills/planner/SKILL.md +3 -3
- package/skills/service-ops/SKILL.md +25 -0
- package/commands/harness-solo.md +0 -103
- package/commands/harness-stop.md +0 -53
- package/commands/harness-team.md +0 -530
- package/scripts/harness-dashboard.sh +0 -509
- package/scripts/harness-goal-init.sh +0 -72
- package/scripts/harness-goal-show.sh +0 -37
- package/scripts/harness-gotcha-memory.sh +0 -348
- package/scripts/harness-monitor.sh +0 -398
- package/scripts/harness-prompt-history.sh +0 -164
- package/scripts/harness-tmux.sh +0 -372
package/bin/init.js
CHANGED
|
@@ -286,7 +286,7 @@ function scaffoldHarness() {
|
|
|
286
286
|
// accumulated `### [C-NNN]` entries.
|
|
287
287
|
const conventionsSrc = path.join(PKG_ROOT, 'conventions');
|
|
288
288
|
if (fs.existsSync(conventionsSrc)) {
|
|
289
|
-
const CONV_ENTRY = /^### \[C
|
|
289
|
+
const CONV_ENTRY = /^### \[C-[A-Z0-9_-]+\]/m;
|
|
290
290
|
const files = fs.readdirSync(conventionsSrc);
|
|
291
291
|
for (const file of files) {
|
|
292
292
|
const destPath = path.join(HARNESS_DIR, 'conventions', file);
|
|
@@ -339,7 +339,7 @@ function scaffoldHarness() {
|
|
|
339
339
|
const progress = JSON.parse(fs.readFileSync(progressPath, 'utf8'));
|
|
340
340
|
if (!progress.version || progress.version < 2) {
|
|
341
341
|
progress.version = 2;
|
|
342
|
-
progress.mode =
|
|
342
|
+
progress.mode = 'company';
|
|
343
343
|
if (!progress.team_state) {
|
|
344
344
|
progress.team_state = { active_teams: 0, paused_at: null, resume_from: null };
|
|
345
345
|
}
|
|
@@ -709,6 +709,59 @@ function installUserPromptSubmitHook() {
|
|
|
709
709
|
}
|
|
710
710
|
}
|
|
711
711
|
|
|
712
|
+
// ─────────────────────────────────────────
|
|
713
|
+
// 3e. Stop hook (auto-chain conductor tick)
|
|
714
|
+
// ─────────────────────────────────────────
|
|
715
|
+
function installStopHook() {
|
|
716
|
+
log('Installing Stop hook (auto-chain conductor tick)...');
|
|
717
|
+
|
|
718
|
+
const settingsDir = path.join(PROJECT_ROOT, '.claude');
|
|
719
|
+
const settingsFile = path.join(settingsDir, 'settings.json');
|
|
720
|
+
|
|
721
|
+
ensureDir(settingsDir);
|
|
722
|
+
|
|
723
|
+
let settings = {};
|
|
724
|
+
if (fileExists(settingsFile)) {
|
|
725
|
+
try {
|
|
726
|
+
settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
727
|
+
} catch (e) {
|
|
728
|
+
log('WARNING: Could not parse existing .claude/settings.json, creating new');
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (!settings.hooks) settings.hooks = {};
|
|
733
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
734
|
+
|
|
735
|
+
const hookCmd = 'bash scripts/harness-stop.sh';
|
|
736
|
+
|
|
737
|
+
const alreadyInstalled = settings.hooks.Stop.some((entry) => {
|
|
738
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
739
|
+
if (Array.isArray(entry.hooks)) {
|
|
740
|
+
return entry.hooks.some(
|
|
741
|
+
(h) => h && h.command && h.command.includes('harness-stop')
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
if (entry.type === 'command' && entry.command) {
|
|
745
|
+
return entry.command.includes('harness-stop');
|
|
746
|
+
}
|
|
747
|
+
return false;
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
if (!alreadyInstalled) {
|
|
751
|
+
settings.hooks.Stop.push({
|
|
752
|
+
matcher: '',
|
|
753
|
+
hooks: [{ type: 'command', command: hookCmd }]
|
|
754
|
+
});
|
|
755
|
+
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
756
|
+
log('Stop hook installed in .claude/settings.json');
|
|
757
|
+
log(' → Conductor 가 running 인 동안 turn 종료 시 자동으로 다음 tick 으로 연쇄');
|
|
758
|
+
log(' → 비활성: .harness/config.json behavior.auto_chain_on_stop = false');
|
|
759
|
+
log(' → 상한: behavior.auto_chain_max_per_sprint (기본 200)');
|
|
760
|
+
} else {
|
|
761
|
+
log('Stop hook already installed');
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
712
765
|
// ─────────────────────────────────────────
|
|
713
766
|
// 4. AGENTS.md + CLAUDE.md
|
|
714
767
|
// ─────────────────────────────────────────
|
|
@@ -734,7 +787,7 @@ function installAgentTeamsEnv() {
|
|
|
734
787
|
log('Agent Teams already enabled');
|
|
735
788
|
}
|
|
736
789
|
|
|
737
|
-
// Add git worktree permission for
|
|
790
|
+
// Add git worktree permission for parallel worker isolation
|
|
738
791
|
if (!settings.permissions) settings.permissions = {};
|
|
739
792
|
if (!settings.permissions.allow) settings.permissions.allow = [];
|
|
740
793
|
const worktreePerms = [
|
|
@@ -753,7 +806,7 @@ function installAgentTeamsEnv() {
|
|
|
753
806
|
if (changed) {
|
|
754
807
|
ensureDir(path.dirname(settingsPath));
|
|
755
808
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
756
|
-
log('Git worktree permissions added for
|
|
809
|
+
log('Git worktree permissions added for parallel worker isolation');
|
|
757
810
|
}
|
|
758
811
|
}
|
|
759
812
|
|
|
@@ -906,70 +959,35 @@ function showHelp() {
|
|
|
906
959
|
Usage:
|
|
907
960
|
npx walwal-harness Initialize project for harness engineering
|
|
908
961
|
npx walwal-harness --force Re-initialize (overwrites existing files)
|
|
909
|
-
npx walwal-harness
|
|
910
|
-
npx walwal-harness team --kill Kill Team Mode tmux session
|
|
911
|
-
npx walwal-harness migrate Apply v3 → v4 migration (mode→auto + Conductor)
|
|
962
|
+
npx walwal-harness migrate Apply migration (always-on company mode)
|
|
912
963
|
npx walwal-harness migrate --dry-run Preview migration changes without applying
|
|
964
|
+
npx walwal-harness verify Verify install integrity
|
|
913
965
|
npx walwal-harness --help Show this help
|
|
914
966
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
967
|
+
Runtime (always-on company mode):
|
|
968
|
+
- Stop 훅이 매 turn 종료 시 다음 부서로 자동 연쇄.
|
|
969
|
+
- 1시간 안전망 wake (선택): bash scripts/harness-wake-install.sh install .
|
|
970
|
+
- 3D 대시보드 (선택): bash scripts/harness-dashboard-up.sh
|
|
971
|
+
- Owner 입력은 GOAL 모호성, escalation, 결과 보고에만 필요합니다.
|
|
920
972
|
|
|
921
973
|
What it does:
|
|
922
974
|
1. Scaffolds .harness/ directory (actions, archive, gotchas, config)
|
|
923
975
|
2. Installs skills to .claude/skills/ (dispatcher, planner, generators, evaluators)
|
|
924
|
-
3.
|
|
925
|
-
4.
|
|
926
|
-
5.
|
|
927
|
-
6.
|
|
928
|
-
7. Creates AGENTS.md + CLAUDE.md symlink
|
|
976
|
+
3. Copies helper scripts to scripts/
|
|
977
|
+
4. Registers SessionStart + UserPromptSubmit + Stop hooks
|
|
978
|
+
5. Installs statusline (persistent 1-line status bar)
|
|
979
|
+
6. Creates AGENTS.md + CLAUDE.md symlink
|
|
929
980
|
|
|
930
981
|
After init:
|
|
931
982
|
1. Restart Claude Code session (/exit → re-enter directory)
|
|
932
983
|
2. Say "하네스 엔지니어링 시작" or /harness-dispatcher
|
|
933
|
-
3. After
|
|
984
|
+
3. After GOAL intake, Conductor runs the company loop autonomously.
|
|
985
|
+
4. (선택) bash scripts/harness-wake-install.sh install . — 1시간마다 자동 wake.
|
|
934
986
|
`);
|
|
935
987
|
}
|
|
936
988
|
|
|
937
989
|
// ─────────────────────────────────────────
|
|
938
|
-
//
|
|
939
|
-
// ─────────────────────────────────────────
|
|
940
|
-
function runTeamStudio() {
|
|
941
|
-
const killMode = args.includes('--kill');
|
|
942
|
-
const scriptsDir = path.join(PKG_ROOT, 'scripts');
|
|
943
|
-
const tmuxScript = path.join(scriptsDir, 'harness-tmux.sh');
|
|
944
|
-
|
|
945
|
-
if (!fs.existsSync(tmuxScript)) {
|
|
946
|
-
log('ERROR: harness-tmux.sh not found.');
|
|
947
|
-
process.exit(1);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
try {
|
|
951
|
-
execSync('which tmux', { stdio: 'ignore' });
|
|
952
|
-
} catch {
|
|
953
|
-
log('ERROR: tmux is required. Install with: brew install tmux');
|
|
954
|
-
process.exit(1);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
if (killMode) {
|
|
958
|
-
const cmd = `bash "${tmuxScript}" --kill`;
|
|
959
|
-
execSync(cmd, { stdio: 'inherit' });
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// Enable Agent Teams in project settings
|
|
964
|
-
installAgentTeamsEnv();
|
|
965
|
-
|
|
966
|
-
const cmd = `bash "${tmuxScript}" "${PROJECT_ROOT}" --team`;
|
|
967
|
-
log('Launching Team Mode Studio...');
|
|
968
|
-
execSync(cmd, { stdio: 'inherit' });
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// ─────────────────────────────────────────
|
|
972
|
-
// Migration — v3 → v4 (mode auto + Conductor ownership)
|
|
990
|
+
// Migration — legacy mode fields → always-on company mode
|
|
973
991
|
// ─────────────────────────────────────────
|
|
974
992
|
const TARGET_PROGRESS_VERSION = 4;
|
|
975
993
|
|
|
@@ -986,9 +1004,10 @@ function detectMigrationNeeded() {
|
|
|
986
1004
|
const memoryTplPath = path.join(PKG_ROOT, 'assets', 'templates', 'memory.md');
|
|
987
1005
|
const flags = {
|
|
988
1006
|
progressV3toV4: false,
|
|
989
|
-
|
|
1007
|
+
configMissingCompanyMode: false,
|
|
990
1008
|
memoryMissingSystemEntries: [],
|
|
991
1009
|
gotchaMissingEntries: {}, // { "<filename>": [G-IDs...] }
|
|
1010
|
+
conventionMissingEntries: {}, // { "<filename>": [C-IDs...] }
|
|
992
1011
|
bundleVersionStale: null, // { current, installed }
|
|
993
1012
|
};
|
|
994
1013
|
|
|
@@ -1012,6 +1031,25 @@ function detectMigrationNeeded() {
|
|
|
1012
1031
|
}
|
|
1013
1032
|
}
|
|
1014
1033
|
|
|
1034
|
+
// Convention entry-level diff: preserve user [C-NNN] entries, append only
|
|
1035
|
+
// bundled system entries such as [C-SYS-*].
|
|
1036
|
+
const conventionsSrc = path.join(PKG_ROOT, 'conventions');
|
|
1037
|
+
const conventionsDest = path.join(HARNESS_DIR, 'conventions');
|
|
1038
|
+
if (fs.existsSync(conventionsSrc) && fs.existsSync(conventionsDest)) {
|
|
1039
|
+
const files = fs.readdirSync(conventionsSrc).filter((f) => f.endsWith('.md') && f !== 'README.md');
|
|
1040
|
+
for (const file of files) {
|
|
1041
|
+
const srcPath = path.join(conventionsSrc, file);
|
|
1042
|
+
const destPath = path.join(conventionsDest, file);
|
|
1043
|
+
if (!fs.existsSync(destPath)) continue;
|
|
1044
|
+
try {
|
|
1045
|
+
const srcIds = extractConventionEntryIds(fs.readFileSync(srcPath, 'utf8'));
|
|
1046
|
+
const dstIds = new Set(extractConventionEntryIds(fs.readFileSync(destPath, 'utf8')));
|
|
1047
|
+
const missing = srcIds.filter((id) => !dstIds.has(id));
|
|
1048
|
+
if (missing.length) flags.conventionMissingEntries[file] = missing;
|
|
1049
|
+
} catch {}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1015
1053
|
// Bundle version stamp: detects fresh package vs. last-applied bundle.
|
|
1016
1054
|
// Even if the 3 schema flags pass, a newer bundle may have introduced new
|
|
1017
1055
|
// gotcha/SKILL/template content the user hasn't seen yet.
|
|
@@ -1032,7 +1070,7 @@ function detectMigrationNeeded() {
|
|
|
1032
1070
|
if (fs.existsSync(configPath)) {
|
|
1033
1071
|
try {
|
|
1034
1072
|
const c = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1035
|
-
if (!c.mode_selection) flags.
|
|
1073
|
+
if (!c.company_mode || c.mode_selection) flags.configMissingCompanyMode = true;
|
|
1036
1074
|
} catch {}
|
|
1037
1075
|
}
|
|
1038
1076
|
if (fs.existsSync(memoryPath) && fs.existsSync(memoryTplPath)) {
|
|
@@ -1086,18 +1124,37 @@ function extractGotchaEntryBlock(md, id) {
|
|
|
1086
1124
|
return m ? m[1].trimEnd() : null;
|
|
1087
1125
|
}
|
|
1088
1126
|
|
|
1127
|
+
// Returns convention entry IDs ("[C-001]", "[C-SYS-*]" etc.) found in markdown.
|
|
1128
|
+
function extractConventionEntryIds(md) {
|
|
1129
|
+
const re = /^#{2,3}\s+\[(C-[A-Z0-9_-]+)\]/gm;
|
|
1130
|
+
const ids = [];
|
|
1131
|
+
let m;
|
|
1132
|
+
while ((m = re.exec(md)) !== null) ids.push(m[1]);
|
|
1133
|
+
return ids;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function extractConventionEntryBlock(md, id) {
|
|
1137
|
+
const escId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1138
|
+
const re = new RegExp(
|
|
1139
|
+
`(^#{2,3}\\s+\\[${escId}\\][\\s\\S]*?)(?=^#{2,3}\\s+\\[C-|$(?![\\s\\S]))`,
|
|
1140
|
+
'm',
|
|
1141
|
+
);
|
|
1142
|
+
const m = md.match(re);
|
|
1143
|
+
return m ? m[1].trimEnd() : null;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1089
1146
|
function showMigrationProposal(flags) {
|
|
1090
1147
|
console.log('');
|
|
1091
1148
|
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
1092
1149
|
console.log('║ walwal-harness v6 — 자동 마이그레이션 사용 가능 ║');
|
|
1093
1150
|
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
1094
1151
|
if (flags.progressV3toV4) {
|
|
1095
|
-
console.log(' • progress.json:
|
|
1096
|
-
console.log('
|
|
1152
|
+
console.log(' • progress.json: legacy mode → "company"');
|
|
1153
|
+
console.log(' legacy mode 값은 보존하지 않고 회사모드로 정규화됩니다.');
|
|
1097
1154
|
}
|
|
1098
|
-
if (flags.
|
|
1099
|
-
console.log(' • config.json:
|
|
1100
|
-
console.log('
|
|
1155
|
+
if (flags.configMissingCompanyMode) {
|
|
1156
|
+
console.log(' • config.json: company_mode 섹션 동기화 가능');
|
|
1157
|
+
console.log(' 사용자 모드 선택 없이 회사모드 병렬 실행으로 고정합니다.');
|
|
1101
1158
|
}
|
|
1102
1159
|
if (flags.memoryMissingSystemEntries && flags.memoryMissingSystemEntries.length) {
|
|
1103
1160
|
console.log(' • memory.md: 시스템 entry 누락 — append 가능');
|
|
@@ -1111,6 +1168,13 @@ function showMigrationProposal(flags) {
|
|
|
1111
1168
|
console.log(` ${file}: [${ids.join(', ')}]`);
|
|
1112
1169
|
}
|
|
1113
1170
|
}
|
|
1171
|
+
const conventionSummary = Object.entries(flags.conventionMissingEntries || {});
|
|
1172
|
+
if (conventionSummary.length) {
|
|
1173
|
+
console.log(' • conventions/: 시스템 entry 누락 — entry-level append 가능 (사용자 [C-NNN] 보존)');
|
|
1174
|
+
for (const [file, ids] of conventionSummary) {
|
|
1175
|
+
console.log(` ${file}: [${ids.join(', ')}]`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1114
1178
|
if (flags.bundleVersionStale) {
|
|
1115
1179
|
const { current, installed } = flags.bundleVersionStale;
|
|
1116
1180
|
console.log(` • bundle: ${installed ?? '(없음)'} → ${current} 스탬프 갱신 필요`);
|
|
@@ -1130,7 +1194,7 @@ function runMigrate(opts = {}) {
|
|
|
1130
1194
|
const gotchaMissingTotal = Object.values(flags.gotchaMissingEntries || {}).reduce((n, a) => n + a.length, 0);
|
|
1131
1195
|
if (
|
|
1132
1196
|
!flags.progressV3toV4 &&
|
|
1133
|
-
!flags.
|
|
1197
|
+
!flags.configMissingCompanyMode &&
|
|
1134
1198
|
(!flags.memoryMissingSystemEntries || flags.memoryMissingSystemEntries.length === 0) &&
|
|
1135
1199
|
gotchaMissingTotal === 0 &&
|
|
1136
1200
|
!flags.bundleVersionStale
|
|
@@ -1138,7 +1202,7 @@ function runMigrate(opts = {}) {
|
|
|
1138
1202
|
console.log('');
|
|
1139
1203
|
let pkgVer = 'unknown';
|
|
1140
1204
|
try { pkgVer = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version; } catch {}
|
|
1141
|
-
log(`이미 최신 — bundle v${pkgVer} 일치, progress v${TARGET_PROGRESS_VERSION}, config.
|
|
1205
|
+
log(`이미 최신 — bundle v${pkgVer} 일치, progress v${TARGET_PROGRESS_VERSION}, config.company_mode, memory 시스템 entry, gotcha entry 모두 sync.`);
|
|
1142
1206
|
return;
|
|
1143
1207
|
}
|
|
1144
1208
|
|
|
@@ -1154,35 +1218,37 @@ function runMigrate(opts = {}) {
|
|
|
1154
1218
|
if (flags.progressV3toV4 && fs.existsSync(progressPath)) {
|
|
1155
1219
|
const original = fs.readFileSync(progressPath, 'utf8');
|
|
1156
1220
|
const p = JSON.parse(original);
|
|
1157
|
-
const oldMode = p.mode ?? '
|
|
1221
|
+
const oldMode = p.mode ?? 'company';
|
|
1158
1222
|
const newP = {
|
|
1159
1223
|
...p,
|
|
1160
1224
|
version: TARGET_PROGRESS_VERSION,
|
|
1161
|
-
mode: '
|
|
1225
|
+
mode: 'company',
|
|
1162
1226
|
mode_decision: {
|
|
1163
1227
|
owner: 'conductor',
|
|
1164
1228
|
decided_at: null,
|
|
1165
|
-
rationale: `migration
|
|
1166
|
-
|
|
1229
|
+
rationale: `migration — legacy mode="${oldMode}" normalized to company`,
|
|
1230
|
+
policy: 'always_company_parallel',
|
|
1231
|
+
user_override: null,
|
|
1167
1232
|
},
|
|
1168
1233
|
};
|
|
1169
|
-
log(` progress.json: version 3 → ${TARGET_PROGRESS_VERSION}, mode "${oldMode}" → "
|
|
1234
|
+
log(` progress.json: version 3 → ${TARGET_PROGRESS_VERSION}, mode "${oldMode}" → "company"`);
|
|
1170
1235
|
if (!dryRun) {
|
|
1171
1236
|
fs.writeFileSync(path.join(backupDir, 'progress.json'), original);
|
|
1172
1237
|
fs.writeFileSync(progressPath, JSON.stringify(newP, null, 2) + '\n');
|
|
1173
1238
|
}
|
|
1174
1239
|
}
|
|
1175
1240
|
|
|
1176
|
-
// 2. config.json — inject
|
|
1241
|
+
// 2. config.json — inject company_mode from template if missing
|
|
1177
1242
|
const configPath = path.join(HARNESS_DIR, 'config.json');
|
|
1178
1243
|
const tplPath = path.join(PKG_ROOT, 'assets', 'templates', 'config.json');
|
|
1179
|
-
if (flags.
|
|
1244
|
+
if (flags.configMissingCompanyMode && fs.existsSync(configPath) && fs.existsSync(tplPath)) {
|
|
1180
1245
|
const original = fs.readFileSync(configPath, 'utf8');
|
|
1181
1246
|
const c = JSON.parse(original);
|
|
1182
1247
|
const tpl = JSON.parse(fs.readFileSync(tplPath, 'utf8'));
|
|
1183
|
-
if (tpl.
|
|
1184
|
-
c.
|
|
1185
|
-
|
|
1248
|
+
if (tpl.company_mode) {
|
|
1249
|
+
c.company_mode = tpl.company_mode;
|
|
1250
|
+
delete c.mode_selection;
|
|
1251
|
+
log(' config.json: company_mode 섹션 주입 (always-on company parallel)');
|
|
1186
1252
|
if (!dryRun) {
|
|
1187
1253
|
fs.writeFileSync(path.join(backupDir, 'config.json'), original);
|
|
1188
1254
|
fs.writeFileSync(configPath, JSON.stringify(c, null, 2) + '\n');
|
|
@@ -1237,6 +1303,30 @@ function runMigrate(opts = {}) {
|
|
|
1237
1303
|
}
|
|
1238
1304
|
}
|
|
1239
1305
|
|
|
1306
|
+
// 4b. convention entry merge — append bundled system [C-*] entries while
|
|
1307
|
+
// preserving user-authored [C-NNN] history.
|
|
1308
|
+
const conventionMissing = flags.conventionMissingEntries || {};
|
|
1309
|
+
for (const [file, missingIds] of Object.entries(conventionMissing)) {
|
|
1310
|
+
if (!missingIds.length) continue;
|
|
1311
|
+
const srcPath = path.join(PKG_ROOT, 'conventions', file);
|
|
1312
|
+
const destPath = path.join(HARNESS_DIR, 'conventions', file);
|
|
1313
|
+
if (!fs.existsSync(srcPath) || !fs.existsSync(destPath)) continue;
|
|
1314
|
+
const original = fs.readFileSync(destPath, 'utf8');
|
|
1315
|
+
const tpl = fs.readFileSync(srcPath, 'utf8');
|
|
1316
|
+
const blocks = [];
|
|
1317
|
+
for (const id of missingIds) {
|
|
1318
|
+
const block = extractConventionEntryBlock(tpl, id);
|
|
1319
|
+
if (block) blocks.push(block);
|
|
1320
|
+
}
|
|
1321
|
+
if (!blocks.length) continue;
|
|
1322
|
+
log(` conventions/${file}: 시스템 entry ${blocks.length}개 append (${missingIds.join(', ')})`);
|
|
1323
|
+
if (!dryRun) {
|
|
1324
|
+
fs.writeFileSync(path.join(backupDir, `conventions-${file}`), original);
|
|
1325
|
+
const sep = original.endsWith('\n') ? '\n' : '\n\n';
|
|
1326
|
+
fs.writeFileSync(destPath, original + sep + blocks.join('\n\n') + '\n');
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1240
1330
|
// 5. Bundle version stamp — record which package version was last applied.
|
|
1241
1331
|
if (flags.bundleVersionStale) {
|
|
1242
1332
|
const { current, installed } = flags.bundleVersionStale;
|
|
@@ -1252,7 +1342,7 @@ function runMigrate(opts = {}) {
|
|
|
1252
1342
|
} else {
|
|
1253
1343
|
log(`Migration 완료. 백업: ${backupDir}`);
|
|
1254
1344
|
log('Conductor 가 다음 sprint 시작 시 자동으로 모드 결정합니다.');
|
|
1255
|
-
log('
|
|
1345
|
+
log('회사모드는 기본값이며 사용자 override 는 사용하지 않습니다.');
|
|
1256
1346
|
}
|
|
1257
1347
|
console.log('');
|
|
1258
1348
|
}
|
|
@@ -1267,6 +1357,7 @@ function runVerify() {
|
|
|
1267
1357
|
'generator-backend', 'generator-frontend', 'generator-designer', 'generator-devops',
|
|
1268
1358
|
'evaluator-code-quality', 'evaluator-functional', 'evaluator-visual',
|
|
1269
1359
|
'evaluator-architecture', 'evaluator-security',
|
|
1360
|
+
'coo-developer', 'documentationer',
|
|
1270
1361
|
'brainstorming',
|
|
1271
1362
|
];
|
|
1272
1363
|
const requiredFrontmatter = ['name', 'description'];
|
|
@@ -1325,17 +1416,17 @@ function runVerify() {
|
|
|
1325
1416
|
}
|
|
1326
1417
|
}
|
|
1327
1418
|
|
|
1328
|
-
// 3) config.json
|
|
1419
|
+
// 3) config.json company_mode
|
|
1329
1420
|
const configPath = path.join(HARNESS_DIR, 'config.json');
|
|
1330
1421
|
if (fs.existsSync(configPath)) {
|
|
1331
1422
|
try {
|
|
1332
1423
|
const c = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1333
|
-
if (!c.
|
|
1334
|
-
issues.push(' ✗ config.json:
|
|
1424
|
+
if (!c.company_mode) {
|
|
1425
|
+
issues.push(' ✗ config.json: company_mode 누락 → npx walwal-harness migrate');
|
|
1335
1426
|
fail++;
|
|
1336
1427
|
} else {
|
|
1337
1428
|
pass++;
|
|
1338
|
-
log(` config.json:
|
|
1429
|
+
log(` config.json: company_mode.owner=${c.company_mode.owner} OK`);
|
|
1339
1430
|
}
|
|
1340
1431
|
} catch {}
|
|
1341
1432
|
}
|
|
@@ -1378,11 +1469,6 @@ function main() {
|
|
|
1378
1469
|
return;
|
|
1379
1470
|
}
|
|
1380
1471
|
|
|
1381
|
-
if (subcommand === 'team') {
|
|
1382
|
-
runTeamStudio();
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
1472
|
if (subcommand === 'migrate') {
|
|
1387
1473
|
runMigrate({ dryRun: args.includes('--dry-run') });
|
|
1388
1474
|
return;
|
|
@@ -1393,10 +1479,14 @@ function main() {
|
|
|
1393
1479
|
return;
|
|
1394
1480
|
}
|
|
1395
1481
|
|
|
1396
|
-
// Legacy subcommands —
|
|
1397
|
-
if (subcommand === '
|
|
1398
|
-
|
|
1399
|
-
|
|
1482
|
+
// Legacy subcommands — removed as of v6.x (always-on company mode).
|
|
1483
|
+
if (subcommand === 'company' || subcommand === 'studio' ||
|
|
1484
|
+
subcommand === 'studio-v4' || subcommand === 'v4') {
|
|
1485
|
+
log(`NOTE: "${subcommand}" subcommand was removed.`);
|
|
1486
|
+
log('회사모드는 항상 켜져 있는 기본 런타임입니다. 별도 launch 명령은 필요 없습니다.');
|
|
1487
|
+
log(' - turn 자동 연쇄: Stop 훅 (자동)');
|
|
1488
|
+
log(' - 1시간 안전망 wake: bash scripts/harness-wake-install.sh install .');
|
|
1489
|
+
log(' - 시각화: bash scripts/harness-dashboard-up.sh');
|
|
1400
1490
|
return;
|
|
1401
1491
|
}
|
|
1402
1492
|
|
|
@@ -1418,6 +1508,7 @@ function main() {
|
|
|
1418
1508
|
installSessionHook();
|
|
1419
1509
|
installStatusline();
|
|
1420
1510
|
installUserPromptSubmitHook();
|
|
1511
|
+
installStopHook();
|
|
1421
1512
|
installAgentTeamsEnv();
|
|
1422
1513
|
setupAgentsMd();
|
|
1423
1514
|
checkPlaywrightMcp();
|
|
@@ -1428,7 +1519,7 @@ function main() {
|
|
|
1428
1519
|
const gotchaMissing = Object.keys(migFlags.gotchaMissingEntries || {}).length > 0;
|
|
1429
1520
|
const hasContentDrift =
|
|
1430
1521
|
migFlags.progressV3toV4 ||
|
|
1431
|
-
migFlags.
|
|
1522
|
+
migFlags.configMissingCompanyMode ||
|
|
1432
1523
|
(migFlags.memoryMissingSystemEntries && migFlags.memoryMissingSystemEntries.length) ||
|
|
1433
1524
|
gotchaMissing;
|
|
1434
1525
|
if (hasContentDrift) {
|
|
@@ -1462,14 +1553,13 @@ function main() {
|
|
|
1462
1553
|
log('║ Then say: "하네스 엔지니어링 시작" ║');
|
|
1463
1554
|
log('║ Or invoke: /harness-dispatcher ║');
|
|
1464
1555
|
log('║ ║');
|
|
1465
|
-
log('║ 기본은
|
|
1466
|
-
log('║ 병렬 3팀 실행을 원하면 파이프라인 확정 후 /harness-team ║');
|
|
1556
|
+
log('║ 기본은 회사모드: 병렬 · 자율 진행. 추가 입력 불필요. ║');
|
|
1467
1557
|
log('╚═══════════════════════════════════════════════════════════╝');
|
|
1468
1558
|
} else {
|
|
1469
1559
|
log('Next steps:');
|
|
1470
1560
|
log(' 1. Restart Claude Code session (/exit → re-enter directory)');
|
|
1471
1561
|
log(' 2. Say "하네스 엔지니어링 시작" or /harness-dispatcher');
|
|
1472
|
-
log(' 3.
|
|
1562
|
+
log(' 3. 회사모드가 자동으로 병렬 실행합니다. 진행 여부를 묻지 않습니다.');
|
|
1473
1563
|
}
|
|
1474
1564
|
console.log('');
|
|
1475
1565
|
}
|
package/conventions/shared.md
CHANGED
|
@@ -22,3 +22,37 @@ docmeta:
|
|
|
22
22
|
> 사용자가 직접 편집해도 무방합니다. 항목 형식은 `.harness/conventions/README.md` 참고.
|
|
23
23
|
|
|
24
24
|
<!-- 항목이 추가되면 아래에 기록됩니다 -->
|
|
25
|
+
|
|
26
|
+
### [C-SYS-AUTONOMOUS-DEPLOY-AFTER-FIX] 코드 수정 후 운영 반영은 회사가 자율 처리
|
|
27
|
+
|
|
28
|
+
- **Status**: verified
|
|
29
|
+
- **Date**: 2026-05-10
|
|
30
|
+
- **Scope**: all agents
|
|
31
|
+
- **Rule**: 어떤 에이전트든 코드를 수정한 직후 운영 반영이 필요하면 build, restart, migration, cron/launchd 재등록, health check, ops-report까지 회사가 자율 처리한다. CTO, Service-Ops, Generator-DevOps 사이 협의는 내부 handoff로 처리하고 Owner에게는 시작 통지와 결과 보고만 한다.
|
|
32
|
+
- **Why**: Owner 입력은 GOAL/사고/결과 확인용이지 운영 펌프 신호가 아니다. 최초 GOAL 이후 회사는 내부 owner를 정해 복구와 배포를 진행해야 한다.
|
|
33
|
+
- **Apply**: 코드/설정 변경 후 운영 반영이 필요하면 Meeting-Manager 또는 Conductor는 `required_execution`에 owner, deliverable, verifier를 명시하고, Service-Ops가 health evidence를 갱신할 때까지 `waiting_owner`로 멈추지 않는다.
|
|
34
|
+
|
|
35
|
+
### [C-SYS-HOURLY-REPORT-AS-MEETING-MINUTES] hourly 보고서는 부서 회의록 형식이다
|
|
36
|
+
|
|
37
|
+
- **Status**: verified
|
|
38
|
+
- **Date**: 2026-05-10
|
|
39
|
+
- **Scope**: meeting-manager, service-ops, conductor
|
|
40
|
+
- **Rule**: `harness-hourly-review.sh`가 작성하는 md 및 Telegram 송신 대상은 단순 service health/KPI dump가 아니라 CEO/COO/CTO/CQO/Service-Ops의 role position, discussion, decision JSON, action items, required execution을 포함한 회의록이어야 한다.
|
|
41
|
+
- **Why**: Owner가 확인하려는 것은 서버 운용현황 자체가 아니라 회사가 GOAL을 향해 어떤 상황을 어떻게 타진하고 어떤 실행 결정을 내렸는지다.
|
|
42
|
+
- **Anti-pattern**: health table만 있고 `required_execution`, owner, verifier, next-tick deliverable이 없으면 회의가 아니라 paperwork로 분류한다.
|
|
43
|
+
|
|
44
|
+
### [C-SYS-EXEC-ROLE-CONTRACT] 회사 구조와 임원 역할 계약은 하네스 공통 규칙이다
|
|
45
|
+
|
|
46
|
+
- **Status**: verified
|
|
47
|
+
- **Date**: 2026-05-10
|
|
48
|
+
- **Scope**: all agents
|
|
49
|
+
- **Rule**: walwal-harness는 "회사 구조 + 자율 구조 + 하네스"를 구현한다. CEO/COO/CTO/CQO/Service-Ops의 역할, 해야 할 일, 하면 안 되는 일은 개별 회의 프롬프트의 장식이 아니라 모든 에이전트가 세션 시작 시 적용해야 하는 하네스 공통 규칙이다.
|
|
50
|
+
- **Role contract**:
|
|
51
|
+
- `Dispatcher/CEO`: Owner와의 유일한 외부 창구. GOAL, 사업 우선순위, Owner escalation 필요성을 판정한다. 내부 해결 가능한 일을 Owner 대기로 끝내지 않는다.
|
|
52
|
+
- `Planner/COO`: GOAL을 work package, queue, 가설, plan으로 바꾼다. planning_drift/goal_drift를 판정하고 다음 operating cycle을 정의한다.
|
|
53
|
+
- `CTO`: 구현, 아키텍처, 기술선택, runtime recovery 책임자다. 서버 down이나 구현 drift를 Service-Ops 알림으로 방치하지 않고 복구/핫픽스 owner를 지정한다.
|
|
54
|
+
- `CQO`: 품질, 회귀, 검증 기준 책임자다. evidence 없는 PASS, 낙관론, 미검증 복구 주장을 통과시키지 않는다.
|
|
55
|
+
- `Service-Ops`: 운영 신호, KPI, incident, monitor cadence 책임자다. 경고를 혼자 남기고 끝내지 않고 meeting-manager를 통해 CTO/CQO action으로 연결한다.
|
|
56
|
+
- `Meeting-Manager`: 임원 회의를 소집/기록/디스패치한다. 회의록에는 각 role의 Position, Evidence, Action이 있어야 한다.
|
|
57
|
+
- **Autonomy rule**: 최초 GOAL 이후 Owner 입력은 interrupt/additional request다. 회사는 `waiting_owner`로 멈추지 않고 `meeting-manager`와 `conductor`를 통해 다음 내부 owner로 진행한다.
|
|
58
|
+
- **Evidence rule**: "회의했다"는 `.harness/actions/meetings/<id>/meeting-<id>.md`에 임원별 입장, 토론, decision JSON, action items가 있을 때만 말할 수 있다.
|
package/gotchas/conductor.md
CHANGED
|
@@ -20,3 +20,19 @@ docmeta:
|
|
|
20
20
|
> `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
|
|
21
21
|
|
|
22
22
|
<!-- 항목이 추가되면 아래에 기록됩니다 -->
|
|
23
|
+
|
|
24
|
+
### [G-001] Team mode 큐 누락 금지
|
|
25
|
+
|
|
26
|
+
- Status: verified
|
|
27
|
+
- Date: 2026-05-08
|
|
28
|
+
- Trigger: `progress.json.mode == "team"` 인데 `.harness/actions/feature-queue.json` 이 없거나 teams 가 모두 idle 인 채 단일 `next_agent` 만 진행
|
|
29
|
+
- Rule: Team mode 진입 시 반드시 `scripts/harness-queue-manager.sh init|recover` 로 queue 를 준비하고, `auto-dispatch` 결과의 모든 pair 를 background worker 로 spawn 해야 한다. tmux monitor 만 띄우고 worker spawn 없이 종료하면 실패다.
|
|
30
|
+
- Evidence: okx/seller 최신 배포본에서 `mode=team` 이지만 `feature-queue.json` 이 없어 병렬 worker 가 시작될 수 없었다.
|
|
31
|
+
|
|
32
|
+
### [G-002] Parallel tracks 직렬화 금지
|
|
33
|
+
|
|
34
|
+
- Status: verified
|
|
35
|
+
- Date: 2026-05-08
|
|
36
|
+
- Trigger: `tracks.length >= 2` 인 회의 결정 후 첫 트랙만 running, 나머지는 pending 으로 두고 완료 후 다음 트랙을 dispatch
|
|
37
|
+
- Rule: parallel tracks 는 같은 tick 에 모든 독립 트랙을 running/spawn 대상으로 노출해야 한다. 완료 순서만 rendezvous 에서 join 하며, dispatch 자체를 sequence 로 만들면 안 된다.
|
|
38
|
+
- Evidence: v6.2 fork-join 문서와 달리 conductor 구현/meeting-manager 문구가 “첫 번째 트랙 완료 후 다음 pending” 형태로 남아 있었다.
|
package/gotchas/dispatcher.md
CHANGED
|
@@ -20,3 +20,19 @@ docmeta:
|
|
|
20
20
|
> `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
|
|
21
21
|
|
|
22
22
|
<!-- 항목이 추가되면 아래에 기록됩니다 -->
|
|
23
|
+
|
|
24
|
+
### [G-001] Owner GOAL 반복 확인 질문 금지
|
|
25
|
+
|
|
26
|
+
- Status: verified
|
|
27
|
+
- Date: 2026-05-08
|
|
28
|
+
- Trigger: Owner 가 이미 GOAL 을 명시했거나 `.harness/actions/pipeline.json`, `.harness/actions/goals.md`, `AGENTS.md Active dispatch` 중 하나가 현재 목표를 설명하는데도 “어떤 방식으로 진행할까요”, “다음 단계로 갈까요”, “브레인스토밍할까요”를 반복 질문
|
|
29
|
+
- Rule: GOAL 이 존재하면 합리적 해석으로 회사 루프를 진행한다. 질문은 GOAL 자체가 상호 배타적인 두 방향으로 해석되어 자동 선택이 위험할 때 한 번만 허용한다.
|
|
30
|
+
- Evidence: okx/seller에서 Owner가 목표를 준 뒤에도 같은 성격의 확인 질문이 3회 반복되어 “알아서 동작하는 회사” 모델을 깨뜨렸다.
|
|
31
|
+
|
|
32
|
+
### [G-SYS-ROLE-CONTRACT-NOT-PROMPT-ONLY] 역할 계약을 일회성 프롬프트에만 넣지 말 것
|
|
33
|
+
|
|
34
|
+
- Status: verified
|
|
35
|
+
- Date: 2026-05-10
|
|
36
|
+
- Trigger: CEO/COO/CTO/CQO/Service-Ops가 각자 해야 할 일과 금지 사항을 이해하지 못하는데, 이를 개별 wake prompt나 회의 템플릿에만 임시로 적어 하위 에이전트에 자동 적용되지 않음.
|
|
37
|
+
- Rule: 회사 구조, 자율 구조, 역할별 해야 할 일/하면 안 되는 일은 하네스 공통 convention/gotcha/memory에 들어가야 한다. 개별 프롬프트는 이 SoT를 읽게 해야 하며, SoT 자체를 대체하면 안 된다.
|
|
38
|
+
- Fix: 구조적 규칙은 `conventions/shared.md`, 역할별 실수는 `gotchas/<role>.md`, 모든 에이전트 공통 교훈은 `memory.md`에 시스템 entry로 승격한다.
|
|
@@ -20,3 +20,11 @@ docmeta:
|
|
|
20
20
|
> `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
|
|
21
21
|
|
|
22
22
|
<!-- 항목이 추가되면 아래에 기록됩니다 -->
|
|
23
|
+
|
|
24
|
+
### [G-SYS-MINUTES-NOT-OPS-TABLE] 회의록을 Service-Ops 상태표로 대체하지 말 것
|
|
25
|
+
|
|
26
|
+
- Status: verified
|
|
27
|
+
- Date: 2026-05-10
|
|
28
|
+
- Trigger: hourly report 또는 meeting record가 서버 health table, queue 상태, "continue loop" 한 줄만 남기고 CEO/COO/CTO/CQO/Service-Ops가 무엇을 논의했는지 보여주지 않음.
|
|
29
|
+
- Rule: Meeting-Manager의 산출물은 운영 리포트가 아니라 임원 회의록이다. `## Role Briefs` 또는 `## Required Role Positions`에 Dispatcher/CEO, Planner/COO, CTO, CQO, Service-Ops 전원의 Position/Evidence/Action이 있어야 하며, `## Discussion`, `## Decision JSON`, `## Action Items`가 없으면 회의 완료로 보고하지 않는다.
|
|
30
|
+
- Fix: Service-Ops health table은 evidence로만 넣고, 토론과 decision owner/action_type으로 내부 실행 owner를 지정한다.
|
package/gotchas/service-ops.md
CHANGED
|
@@ -20,3 +20,11 @@ docmeta:
|
|
|
20
20
|
> `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
|
|
21
21
|
|
|
22
22
|
<!-- 항목이 추가되면 아래에 기록됩니다 -->
|
|
23
|
+
|
|
24
|
+
### [G-SYS-ALERT-MUST-BECOME-MEETING-ACTION] 운영 경고를 혼자 남기고 끝내지 말 것
|
|
25
|
+
|
|
26
|
+
- Status: verified
|
|
27
|
+
- Date: 2026-05-10
|
|
28
|
+
- Trigger: Service-Ops가 서버 down, health fail, KPI drift를 감지했지만 ops-report만 쓰고 CTO/CQO/Meeting-Manager action으로 연결하지 않음.
|
|
29
|
+
- Rule: Service-Ops의 경고는 최종 산출물이 아니라 회의 evidence다. red-alert/down/degraded가 있으면 incident-war-room 또는 followup-review를 요구하고, CTO runtime-recovery action과 CQO evidence gate가 회의록에 남도록 해야 한다.
|
|
30
|
+
- Fix: `.service_ops.incident.open[]`, ops-report path, health table을 evidence로 제출하고 `.meetings.requested_type`/`.meetings.requested_reason` 또는 meeting decision을 통해 다음 owner를 지정한다.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "6.1.
|
|
4
|
-
"description": "Production harness for AI agent engineering — NEXUS-adapted company metaphor (Dispatcher/CEO + Conductor + Meeting-Manager + COO/Planner + CTO + CQO + Service-Ops).
|
|
3
|
+
"version": "6.1.6",
|
|
4
|
+
"description": "Production harness for AI agent engineering — NEXUS-adapted company metaphor (Dispatcher/CEO + Conductor + Meeting-Manager + COO/Planner + CTO + CQO + Service-Ops). Always-on company mode, parallel worker pool, Brainstormer, Planner, Generator(BE/FE/Designer/DevOps), Evaluator chain (Code-Quality → Functional → Visual + Architecture/Security). Supports React, Next.js, and Flutter FE stacks.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
|
7
7
|
},
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"codex",
|
|
19
19
|
"skill",
|
|
20
20
|
"command",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"company-mode",
|
|
22
|
+
"parallel-workers",
|
|
23
23
|
"brainstorming",
|
|
24
24
|
"planner",
|
|
25
25
|
"generator",
|