@walwal-harness/cli 6.0.4 → 6.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -29,6 +29,27 @@ docmeta:
29
29
 
30
30
  # Changelog
31
31
 
32
+ ## 6.0.5 — migrate 가 gotcha entry / bundle version 까지 sync (2026-05-07)
33
+
34
+ ### Why
35
+ Owner 보고 — `npm install @walwal-harness/cli@latest && npx walwal-harness migrate` 가 "이미 최신 버전입니다" 로 종료. 그러나 v6.0.4 에서 새로 추가된 `gotchas/conductor.md` 의 [G-005], [G-006], `gotchas/service-ops.md`, `gotchas/generator-frontend.md [G-001]` 가 사용자 프로젝트에 merge 되지 않음. **존재 여부 (3 항목 — progress v4 / config.mode_selection / M-NEXUS-P3) 만 확인하고 콘텐츠는 비교 안 함** → 본질적 버그.
36
+
37
+ ### Changes
38
+ - **bin/init.js `detectMigrationNeeded`** — 두 신규 시그널 추가:
39
+ - `gotchaMissingEntries`: `gotchas/*.md` 별로 패키지 → 사용자 [G-NNN] entry 차이 산출 (memory.md 패턴과 동일)
40
+ - `bundleVersionStale`: `.harness/.bundle-version` 스탬프와 패키지 버전 비교
41
+ - **bin/init.js `runMigrate`** — gotcha entry append + bundle stamp 갱신 단계 추가. 사용자가 직접 추가한 [G-NNN] entry 는 절대 건드리지 않으며, 패키지의 신규 시스템 entry 만 끝에 추가.
42
+ - **bin/init.js `extractGotchaEntryBlock` / `extractEntryBlock`** — JS 정규식 `\Z` 미지원 fix. 마지막 entry 가 추출 누락되던 버그 동시 해결 (v6.0.2 부터 잠재).
43
+ - **bin/init.js main flow** — 콘텐츠 드리프트 없이 stamp 만 누락된 경우 init 종료 시 자동 갱신. stamp 만으로 매번 migrate 권유 알림이 뜨는 것 방지.
44
+
45
+ ### Migration
46
+ ```bash
47
+ npm install @walwal-harness/cli@latest
48
+ npx walwal-harness migrate # gotchas/conductor.md G-005/G-006, gotchas/service-ops.md, gotchas/generator-frontend.md G-001 자동 append + stamp 갱신
49
+ npx walwal-harness verify
50
+ ```
51
+ 사용자 [G-NNN] entry 는 보존되고 시스템 entry 만 끝에 append. 변경 전 `.harness/archive/migration-<ts>/gotchas-<file>` 백업.
52
+
32
53
  ## 6.0.4 — Sub-skill 협업 + Service-Ops 상시 + Team 병렬 spawn (2026-05-07)
33
54
 
34
55
  ### Why
package/bin/init.js CHANGED
@@ -988,7 +988,41 @@ function detectMigrationNeeded() {
988
988
  progressV3toV4: false,
989
989
  configMissingModeSelection: false,
990
990
  memoryMissingSystemEntries: [],
991
+ gotchaMissingEntries: {}, // { "<filename>": [G-IDs...] }
992
+ bundleVersionStale: null, // { current, installed }
991
993
  };
994
+
995
+ // Gotcha entry-level diff: for each bundled gotcha file, compare entry IDs.
996
+ // 사용자가 직접 추가한 [G-NNN] 은 절대 건드리지 않으며, 패키지에서 새로
997
+ // 도입된 시스템 entry 만 append 대상.
998
+ const gotchasSrc = path.join(PKG_ROOT, 'gotchas');
999
+ const gotchasDest = path.join(HARNESS_DIR, 'gotchas');
1000
+ if (fs.existsSync(gotchasSrc) && fs.existsSync(gotchasDest)) {
1001
+ const files = fs.readdirSync(gotchasSrc).filter((f) => f.endsWith('.md') && f !== 'README.md');
1002
+ for (const file of files) {
1003
+ const srcPath = path.join(gotchasSrc, file);
1004
+ const destPath = path.join(gotchasDest, file);
1005
+ if (!fs.existsSync(destPath)) continue; // installSkills/init 이 처리
1006
+ try {
1007
+ const srcIds = extractGotchaEntryIds(fs.readFileSync(srcPath, 'utf8'));
1008
+ const dstIds = new Set(extractGotchaEntryIds(fs.readFileSync(destPath, 'utf8')));
1009
+ const missing = srcIds.filter((id) => !dstIds.has(id));
1010
+ if (missing.length) flags.gotchaMissingEntries[file] = missing;
1011
+ } catch {}
1012
+ }
1013
+ }
1014
+
1015
+ // Bundle version stamp: detects fresh package vs. last-applied bundle.
1016
+ // Even if the 3 schema flags pass, a newer bundle may have introduced new
1017
+ // gotcha/SKILL/template content the user hasn't seen yet.
1018
+ try {
1019
+ const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
1020
+ const stampPath = path.join(HARNESS_DIR, '.bundle-version');
1021
+ const installed = fs.existsSync(stampPath) ? fs.readFileSync(stampPath, 'utf8').trim() : null;
1022
+ if (installed !== pkg.version) {
1023
+ flags.bundleVersionStale = { current: pkg.version, installed };
1024
+ }
1025
+ } catch {}
992
1026
  if (fs.existsSync(progressPath)) {
993
1027
  try {
994
1028
  const p = JSON.parse(fs.readFileSync(progressPath, 'utf8'));
@@ -1025,7 +1059,29 @@ function extractEntryIds(md) {
1025
1059
  // Extracts a single entry block (heading + body until next ### or EOF) from the template.
1026
1060
  function extractEntryBlock(md, id) {
1027
1061
  const escId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1028
- const re = new RegExp(`(^### \\[${escId}\\][\\s\\S]*?)(?=^### \\[|\\Z)`, 'm');
1062
+ const re = new RegExp(`(^### \\[${escId}\\][\\s\\S]*?)(?=^### \\[|$(?![\\s\\S]))`, 'm');
1063
+ const m = md.match(re);
1064
+ return m ? m[1].trimEnd() : null;
1065
+ }
1066
+
1067
+ // Returns gotcha entry IDs ("[G-001]", "[G-NEXUS-P3]" etc.) found in a markdown body.
1068
+ // Supports both `### [G-NNN]` and `## [G-NNN] ...` heading levels.
1069
+ function extractGotchaEntryIds(md) {
1070
+ const re = /^#{2,3}\s+\[(G-[A-Z0-9_-]+)\]/gm;
1071
+ const ids = [];
1072
+ let m;
1073
+ while ((m = re.exec(md)) !== null) ids.push(m[1]);
1074
+ return ids;
1075
+ }
1076
+
1077
+ // Extracts a single gotcha entry block from a markdown body.
1078
+ function extractGotchaEntryBlock(md, id) {
1079
+ const escId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1080
+ // JS lacks \Z; emulate end-of-string via (?![\s\S]) negative lookahead.
1081
+ const re = new RegExp(
1082
+ `(^#{2,3}\\s+\\[${escId}\\][\\s\\S]*?)(?=^#{2,3}\\s+\\[G-|$(?![\\s\\S]))`,
1083
+ 'm',
1084
+ );
1029
1085
  const m = md.match(re);
1030
1086
  return m ? m[1].trimEnd() : null;
1031
1087
  }
@@ -1048,6 +1104,17 @@ function showMigrationProposal(flags) {
1048
1104
  console.log(' [' + flags.memoryMissingSystemEntries.join(', ') + ']');
1049
1105
  console.log(' 사용자 [M-NNN] entry 는 보존, 시스템 entry 만 끝에 추가.');
1050
1106
  }
1107
+ const gotchaSummary = Object.entries(flags.gotchaMissingEntries || {});
1108
+ if (gotchaSummary.length) {
1109
+ console.log(' • gotchas/: 시스템 entry 누락 — entry-level append 가능 (사용자 [G-NNN] 보존)');
1110
+ for (const [file, ids] of gotchaSummary) {
1111
+ console.log(` ${file}: [${ids.join(', ')}]`);
1112
+ }
1113
+ }
1114
+ if (flags.bundleVersionStale) {
1115
+ const { current, installed } = flags.bundleVersionStale;
1116
+ console.log(` • bundle: ${installed ?? '(없음)'} → ${current} 스탬프 갱신 필요`);
1117
+ }
1051
1118
  console.log('');
1052
1119
  console.log(' 적용: npx walwal-harness migrate');
1053
1120
  console.log(' 미리보기: npx walwal-harness migrate --dry-run');
@@ -1060,13 +1127,18 @@ function showMigrationProposal(flags) {
1060
1127
  function runMigrate(opts = {}) {
1061
1128
  const dryRun = opts.dryRun || false;
1062
1129
  const flags = detectMigrationNeeded();
1130
+ const gotchaMissingTotal = Object.values(flags.gotchaMissingEntries || {}).reduce((n, a) => n + a.length, 0);
1063
1131
  if (
1064
1132
  !flags.progressV3toV4 &&
1065
1133
  !flags.configMissingModeSelection &&
1066
- (!flags.memoryMissingSystemEntries || flags.memoryMissingSystemEntries.length === 0)
1134
+ (!flags.memoryMissingSystemEntries || flags.memoryMissingSystemEntries.length === 0) &&
1135
+ gotchaMissingTotal === 0 &&
1136
+ !flags.bundleVersionStale
1067
1137
  ) {
1068
1138
  console.log('');
1069
- log('이미 최신 버전입니다 (progress v' + TARGET_PROGRESS_VERSION + ' + config.mode_selection + memory 시스템 entry 모두 존재).');
1139
+ let pkgVer = 'unknown';
1140
+ 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.mode_selection, memory 시스템 entry, gotcha entry 모두 sync.`);
1070
1142
  return;
1071
1143
  }
1072
1144
 
@@ -1141,6 +1213,39 @@ function runMigrate(opts = {}) {
1141
1213
  }
1142
1214
  }
1143
1215
 
1216
+ // 4. gotcha entry merge — for each bundled gotcha file, append missing
1217
+ // [G-NNN] entries while preserving everything the user already has.
1218
+ const gotchaMissing = flags.gotchaMissingEntries || {};
1219
+ for (const [file, missingIds] of Object.entries(gotchaMissing)) {
1220
+ if (!missingIds.length) continue;
1221
+ const srcPath = path.join(PKG_ROOT, 'gotchas', file);
1222
+ const destPath = path.join(HARNESS_DIR, 'gotchas', file);
1223
+ if (!fs.existsSync(srcPath) || !fs.existsSync(destPath)) continue;
1224
+ const original = fs.readFileSync(destPath, 'utf8');
1225
+ const tpl = fs.readFileSync(srcPath, 'utf8');
1226
+ const blocks = [];
1227
+ for (const id of missingIds) {
1228
+ const block = extractGotchaEntryBlock(tpl, id);
1229
+ if (block) blocks.push(block);
1230
+ }
1231
+ if (!blocks.length) continue;
1232
+ log(` gotchas/${file}: 시스템 entry ${blocks.length}개 append (${missingIds.join(', ')})`);
1233
+ if (!dryRun) {
1234
+ fs.writeFileSync(path.join(backupDir, `gotchas-${file}`), original);
1235
+ const sep = original.endsWith('\n') ? '\n' : '\n\n';
1236
+ fs.writeFileSync(destPath, original + sep + blocks.join('\n\n') + '\n');
1237
+ }
1238
+ }
1239
+
1240
+ // 5. Bundle version stamp — record which package version was last applied.
1241
+ if (flags.bundleVersionStale) {
1242
+ const { current, installed } = flags.bundleVersionStale;
1243
+ log(` .bundle-version: ${installed ?? '(none)'} → ${current}`);
1244
+ if (!dryRun) {
1245
+ fs.writeFileSync(path.join(HARNESS_DIR, '.bundle-version'), current + '\n');
1246
+ }
1247
+ }
1248
+
1144
1249
  console.log('');
1145
1250
  if (dryRun) {
1146
1251
  log('Dry-run 완료 — 실제 변경 적용하려면 `npx walwal-harness migrate` 실행');
@@ -1320,8 +1425,20 @@ function main() {
1320
1425
 
1321
1426
  // v6.0 — propose migration if existing project is on v3
1322
1427
  const migFlags = detectMigrationNeeded();
1323
- if (migFlags.progressV3toV4 || migFlags.configMissingModeSelection) {
1428
+ const gotchaMissing = Object.keys(migFlags.gotchaMissingEntries || {}).length > 0;
1429
+ const hasContentDrift =
1430
+ migFlags.progressV3toV4 ||
1431
+ migFlags.configMissingModeSelection ||
1432
+ (migFlags.memoryMissingSystemEntries && migFlags.memoryMissingSystemEntries.length) ||
1433
+ gotchaMissing;
1434
+ if (hasContentDrift) {
1324
1435
  showMigrationProposal(migFlags);
1436
+ } else if (migFlags.bundleVersionStale) {
1437
+ // 콘텐츠 드리프트 없이 stamp 만 누락/오래됨 → 정합성 차원에서 직접 갱신.
1438
+ try {
1439
+ const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
1440
+ fs.writeFileSync(path.join(HARNESS_DIR, '.bundle-version'), pkg.version + '\n');
1441
+ } catch {}
1325
1442
  }
1326
1443
 
1327
1444
  // v6.0 — Brick Office dashboard one-liner
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "6.0.4",
3
+ "version": "6.0.5",
4
4
  "description": "Production harness for AI agent engineering — NEXUS-adapted company metaphor (Dispatcher/CEO + Conductor + Meeting-Manager + COO/Planner + CTO + CQO + Service-Ops). Solo/Team mode, 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"
@@ -1,109 +0,0 @@
1
- #!/bin/bash
2
- # harness-archive.sh — 스프린트 종료 시 자동 아카이빙
3
- #
4
- # 동작:
5
- # 1. .harness/actions/ 의 스프린트 산출물을 .harness/archive/D-NNN/S-NNN/ 로 이동
6
- # 2. progress.json 의 sprint 상태를 초기화 (신규 dispatch 준비)
7
- # 3. dispatch.id 가 없으면 counter++ 로 새 dispatch 시작
8
- #
9
- # 유지되는 파일 (이동하지 않음):
10
- # - .harness/gotchas/**, .harness/conventions/**, .harness/ref/**
11
- # - .harness/config.json, .harness/memory.md
12
- # - .harness/progress.json (초기화만)
13
- #
14
- # 호출 주체: harness-next.sh (next_agent="archive" 도달 시 자동)
15
-
16
- set -e
17
-
18
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
- source "$SCRIPT_DIR/lib/harness-render-progress.sh" 2>/dev/null || true
20
-
21
- PROJECT_ROOT="${1:-.}"
22
- PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd)"
23
-
24
- PROGRESS="$PROJECT_ROOT/.harness/progress.json"
25
- ACTIONS_DIR="$PROJECT_ROOT/.harness/actions"
26
- ARCHIVE_ROOT="$PROJECT_ROOT/.harness/archive"
27
-
28
- if [ ! -f "$PROGRESS" ]; then
29
- echo "[archive] ERROR: progress.json not found" >&2
30
- exit 1
31
- fi
32
-
33
- command -v jq >/dev/null 2>&1 || { echo "[archive] ERROR: jq required" >&2; exit 1; }
34
-
35
- # ── Read current dispatch/sprint numbers ──
36
- dispatch_counter=$(jq -r '.dispatch.counter // 0' "$PROGRESS")
37
- dispatch_id=$(jq -r '.dispatch.id // empty' "$PROGRESS")
38
- sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS")
39
-
40
- # Ensure dispatch id exists (fallback for legacy progress.json without dispatch)
41
- if [ -z "$dispatch_id" ] || [ "$dispatch_id" = "null" ]; then
42
- if [ "$dispatch_counter" -lt 1 ]; then
43
- dispatch_counter=1
44
- fi
45
- dispatch_id=$(printf 'D-%03d' "$dispatch_counter")
46
- fi
47
-
48
- sprint_id=$(printf 'S-%03d' "$sprint_num")
49
- target_dir="$ARCHIVE_ROOT/$dispatch_id/$sprint_id"
50
-
51
- echo ""
52
- echo " ── Archive ────────────────────────────"
53
- echo " Dispatch : $dispatch_id"
54
- echo " Sprint : $sprint_id"
55
- echo " Target : ${target_dir#$PROJECT_ROOT/}"
56
-
57
- # ── Move actions/ contents into archive ──
58
- if [ -d "$ACTIONS_DIR" ] && [ "$(ls -A "$ACTIONS_DIR" 2>/dev/null)" ]; then
59
- mkdir -p "$target_dir"
60
- moved=0
61
- for f in "$ACTIONS_DIR"/*; do
62
- [ -e "$f" ] || continue
63
- name="$(basename "$f")"
64
- if [ -e "$target_dir/$name" ]; then
65
- # Collision — suffix with timestamp to avoid overwrite
66
- ts=$(date +%Y%m%d-%H%M%S)
67
- mv "$f" "$target_dir/${name%.}.${ts}"
68
- else
69
- mv "$f" "$target_dir/"
70
- fi
71
- moved=$((moved + 1))
72
- done
73
- echo " Moved : $moved file(s)"
74
- else
75
- echo " Moved : 0 file(s) (actions/ empty)"
76
- fi
77
-
78
- # ── Reset progress.json for next dispatch ──
79
- # - sprint → init
80
- # - agents → cleared
81
- # - artifacts → pending
82
- # - dispatch.id cleared (next dispatcher run will allocate new D-NNN)
83
- # - failure → cleared
84
- jq --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
85
- .pipeline = null
86
- | .dispatch.id = null
87
- | .sprint = { number: 0, status: "init", retry_count: 0 }
88
- | .current_agent = null
89
- | .agent_status = "pending"
90
- | .completed_agents = []
91
- | .next_agent = "dispatcher"
92
- | .failure = { agent: null, location: null, message: null, retry_target: null }
93
- | (.artifacts // {}) as $a
94
- | .artifacts = ($a | with_entries(
95
- if (.value | type) == "object"
96
- then .value = { status: "pending", updated_by: null, updated_at: null }
97
- else .
98
- end))
99
- | .updated_at = $now
100
- ' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
101
-
102
- # ── Clear handoff so next session starts fresh ──
103
- HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
104
- echo '{}' > "$HANDOFF"
105
-
106
- echo " Status : archived, progress reset"
107
- echo ""
108
- echo " ✓ 다음 요청은 새로운 dispatch 로 시작합니다."
109
- echo ""
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env bash
2
- # walwal-harness — Brick Office dashboard launcher
3
- #
4
- # 처음 쓰는 사용자도 한 줄로 대시보드를 띄워볼 수 있게 만든 helper.
5
- # walwal-harness CLI 패키지에는 R3F + Next.js 무게 때문에 dashboard 가 미포함이고,
6
- # 본 스크립트가 git sparse-checkout 으로 apps/harness-dashboard/ 만 가져와서
7
- # 사용자 프로젝트 외부 (~/.walwal-harness/dashboard) 에 격리 설치 후 dev 실행한다.
8
- #
9
- # Usage:
10
- # bash scripts/harness-dashboard-up.sh # install + dev (port 3001)
11
- # bash scripts/harness-dashboard-up.sh --port 3050 # override port
12
- # HARNESS_ROOT=/path/to/project bash ... # explicit harness root
13
- set -euo pipefail
14
-
15
- REPO_URL="${WALWAL_HARNESS_REPO:-https://github.com/30eggis/walwal-harness.git}"
16
- DASHBOARD_PATH="apps/harness-dashboard"
17
- LOCAL_DIR="${HOME}/.walwal-harness/dashboard"
18
- PORT="${PORT:-3001}"
19
-
20
- while [[ $# -gt 0 ]]; do
21
- case "$1" in
22
- --port) PORT="$2"; shift 2 ;;
23
- --reinstall) rm -rf "$LOCAL_DIR"; shift ;;
24
- --help|-h)
25
- sed -n '2,16p' "$0"
26
- exit 0 ;;
27
- *) echo "[brick-office] unknown arg: $1" >&2; exit 2 ;;
28
- esac
29
- done
30
-
31
- if [[ -z "${HARNESS_ROOT:-}" ]]; then
32
- HARNESS_ROOT="$(pwd)"
33
- fi
34
- if [[ ! -d "${HARNESS_ROOT}/.harness" ]]; then
35
- echo "[brick-office] FAIL: ${HARNESS_ROOT} 에 .harness/ 가 없습니다."
36
- echo " 먼저 'npx walwal-harness' 로 초기화하세요."
37
- exit 3
38
- fi
39
-
40
- echo "[brick-office] HARNESS_ROOT = ${HARNESS_ROOT}"
41
- echo "[brick-office] dashboard 격리 경로 = ${LOCAL_DIR}"
42
-
43
- if [[ ! -d "${LOCAL_DIR}/${DASHBOARD_PATH}" ]]; then
44
- echo "[brick-office] 첫 실행 — git sparse-checkout 으로 dashboard 만 가져옵니다 (~5MB)."
45
- mkdir -p "${LOCAL_DIR}"
46
- cd "${LOCAL_DIR}"
47
- git init -q
48
- git remote add origin "${REPO_URL}" 2>/dev/null || true
49
- git config core.sparseCheckout true
50
- echo "${DASHBOARD_PATH}/" > .git/info/sparse-checkout
51
- git fetch --depth=1 origin main -q
52
- git checkout main -q
53
- echo "[brick-office] 다운로드 완료."
54
- fi
55
-
56
- cd "${LOCAL_DIR}/${DASHBOARD_PATH}"
57
-
58
- if [[ ! -d node_modules ]]; then
59
- echo "[brick-office] npm install (한 번만 실행, ~30s)..."
60
- npm install --no-audit --no-fund --loglevel=error
61
- fi
62
-
63
- echo ""
64
- echo "╔══════════════════════════════════════════════════════════╗"
65
- echo "║ Brick Office — http://localhost:${PORT}"
66
- echo "║ ║"
67
- echo "║ HARNESS_ROOT 이 가리키는 프로젝트의 .harness/ 상태를 ║"
68
- echo "║ 실시간 (SSE) 시각화합니다. Ctrl+C 로 종료. ║"
69
- echo "╚══════════════════════════════════════════════════════════╝"
70
- echo ""
71
-
72
- HARNESS_ROOT="${HARNESS_ROOT}" npm run dev:dashboard -- -p "${PORT}"