@walwal-harness/cli 7.1.51 → 7.1.53

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
@@ -4,7 +4,7 @@ docmeta:
4
4
  title: walwal-harness CHANGELOG
5
5
  type: output
6
6
  createdAt: 2026-05-07T00:00:00Z
7
- updatedAt: 2026-06-05T00:00:00Z
7
+ updatedAt: 2026-06-24T00:00:00Z
8
8
  source:
9
9
  producer: agent
10
10
  skillId: harness-release
@@ -31,6 +31,19 @@ docmeta:
31
31
 
32
32
  ## Unreleased
33
33
 
34
+ ## 7.1.53 — Write-on-signal documents + `archive/` namespace reclaim (2026-06-24)
35
+ - **`archive/` is now reserved for completed missions, never migrate/init backups.** Every backup snapshot moved out of `.harness/archive/` into a new `.harness/backups/` root: `bin/init.js` (migration snapshot dir, legacy-role migration, pre-harness doc extraction backup, AGENTS/CLAUDE doc backup), `init.sh refresh-ref`, `scripts/init-agents-md.sh`, and `scripts/init-ref-docs.sh`. The Owner expected `archive/` to hold a completed `goal + submission + hot-fix` per goal folder; the harness using it as a migrate backup dump conflicted with that meaning. Files are moved (not deleted), so recovery data is preserved. (Agreement: "archive/ 네임스페이스 탈환".)
36
+ - `.harness/backups/` is added to the managed `.gitignore` block (`assets/templates/gitignore-append.txt`), and **`migrate` now refreshes the host `.gitignore` and best-effort untracks newly-ignored runtime paths** (previously only `init` did this), so migrating projects also stop committing backup/runtime noise.
37
+ - **Hourly review is now write-on-signal.** `scripts/harness-hourly-review.sh` emits a meeting markdown only when the tick carries a real delta — an incident, goal drift, owner escalation, a required-execution contract, a handoff that needs execution, or a change in verdict since the last review. No-delta ticks increment `.meetings.heartbeat_count` instead of writing yet another byte-similar `M-*-hourly` doc (the source of hundreds of boilerplate meeting files). This is safe because the conductor routes off `progress.json` state, not this evidence file, and the no-signal path leaves the last real meeting pointer intact for the `read-decision` fallback. Gated by `company_mode.write_on_signal` (default `true`; `false` restores legacy always-write). (Agreement: 문서 ≠ 텔레메트리 · write-on-signal.)
38
+ - **Wake stops persisting the static prompt.** `scripts/harness-wake.sh` assembles the hourly wake prompt in memory instead of writing a fresh `.harness/actions/wake/wake-<ts>.prompt.md` every tick — those were 100+ byte-identical templates differing only by timestamp/idle/force/review-path. `tmux` mode reuses one overwritten scratch file; each tick appends a single params-only line to `.harness/activity/wake.jsonl` (capped to the last 2000 lines). Per-tick agent logs are written only when an agent actually runs (record mode previously left empty `ops/wake/wake-*.log` files) and are rotated to the most recent 48. (Agreements: 정적 wake.prompt 비영속 · 보존정책 TTL/rotation.)
39
+ - `migrate` injects `company_mode.write_on_signal` into existing `config.json` via a new `configWriteOnSignalMissing` detection, so upgraded projects adopt document suppression automatically.
40
+
41
+ ## 7.1.52 — Single shared dashboard install (drop per-project `node_modules`) (2026-06-23)
42
+ - `scripts/harness-dashboard-up.sh` now installs the Brick Office dashboard **once per version** under `~/.walwal-harness/dashboard/<version>/` (override base with `WALWAL_HARNESS_HOME`) and serves every project from that one copy. Previously each project got its own copy plus a full `npm install`, duplicating identical `node_modules` (~597MB) + `.next` (~75MB) per project — 100 projects cost ~64GB of identical bytes. Now total cost is ~640MB regardless of project count.
43
+ - `npm install` + `next build` run only on the first launch of a given version (guarded by a `.walwal-dashboard-ready` marker and an atomic `mkdir` build lock so concurrent launches don't race). Each project is then served with its own `HARNESS_ROOT` + `--port` via `next start`; since `next start` reads `.next` read-only, many dashboards run concurrently from the same install safely.
44
+ - This is correct because the dashboard code is byte-identical across projects and `HARNESS_ROOT` is resolved at request time (`lib/harness-root.ts`; all routes are `force-dynamic`), so build output never bakes in a project. Supersedes both the pre-7.1.37 per-project-hashed home cache and the 7.1.37 project-local `.harness/dashboard/` copy.
45
+ - `bin/init.js` now writes hook/statusLine commands in `settings.json` as project-root-relative paths (`bash scripts/...`) instead of absolute `PROJECT_ROOT` paths, so moving or renaming a project no longer breaks the harness Stop hook with `No such file` errors.
46
+
34
47
  ## 7.1.48 — Auto-managed `.harness` `.gitignore` + runtime untrack on init (2026-06-05)
35
48
  - `walwal-harness init` now writes/refreshes a marked managed block in the project-root `.gitignore` so machine-specific harness runtime/local state is never committed and never pollutes teammates' diffs. Ignored: live conductor `progress.json`/`progress.log`/`handoff.json`/`events.jsonl`, the `todos/` live queue (`state.json` + `events.jsonl`), `actions/`, `ops/`, `activity/`, `logs/`, `archive/` migration backups, and the `dashboard/` visualization tooling. Durable team knowledge stays tracked: `conventions/`, `gotchas/`, `documents/`, `memories/`, `memory.md`, `shared/`, `config.json`, `HARNESS.md`, `.bundle-version`.
36
49
  - Init also best-effort `git rm --cached`s any of those runtime paths committed before the ignore block existed (files stay on disk), fixing the "`.gitignore` added but already-tracked files keep showing as changes" case.
package/README.md CHANGED
@@ -164,7 +164,7 @@ Run on a different port:
164
164
  bash scripts/harness-dashboard-up.sh --port 3002
165
165
  ```
166
166
 
167
- Force-refresh the project-local dashboard runtime when an older dashboard is still shown:
167
+ Force-rebuild the shared dashboard install when an older dashboard is still shown:
168
168
 
169
169
  ```bash
170
170
  bash scripts/harness-dashboard-up.sh --port 3002 --reinstall
@@ -175,9 +175,9 @@ Dashboard options:
175
175
  | Option | Role |
176
176
  |---|---|
177
177
  | `--port <port>` | Run Brick Office on a custom port, e.g. `3002` |
178
- | `--reinstall` | Delete and rebuild the project-local dashboard runtime under `.harness/dashboard/` |
178
+ | `--reinstall` | Discard and rebuild the shared dashboard install for this version |
179
179
 
180
- The dashboard app copy lives under `.harness/dashboard/` for the current project. Generated dependencies and build output are ignored via `.gitignore`; the runtime files the dashboard reads remain the normal `.harness/` files.
180
+ The dashboard code is identical across all projects, so it is installed **once per version** under `~/.walwal-harness/dashboard/<version>/` and shared by every project — a single `node_modules` plus one production build (`npm install` + `next build`) the first time, then reused. Each project is served with its own `HARNESS_ROOT` and port via `next start`, so you can run many dashboards at once without duplicating dependencies (100 projects still cost ~640MB, not 100×). Override the base directory with `WALWAL_HARNESS_HOME`.
181
181
 
182
182
  ### Company Auto-Chain State
183
183
 
@@ -190,6 +190,8 @@
190
190
  "hourly_wake_model_description": "Claude executor 에서 특정 모델을 강제하려면 HARNESS_WAKE_MODEL 또는 이 값을 사용한다. 비워두면 CLI 기본 모델을 사용한다.",
191
191
  "hourly_wake_mode": "headless",
192
192
  "hourly_wake_mode_description": "headless 는 백그라운드 실행, tmux 는 매시간 별도 tmux 세션, record 는 prompt/log만 생성한다.",
193
+ "write_on_signal": true,
194
+ "write_on_signal_description": "true 이면 hourly-review 가 델타(인시던트/목표드리프트/실행계약/판정 변화)가 있는 틱에서만 meeting 문서를 생성하고, 무신호 틱은 .meetings.heartbeat_count 만 증가시킨다. 문서≠텔레메트리·write-on-signal 합의에 따라 동일 boilerplate 문서 폭증을 막는다. false 로 두면 매 틱 문서를 남기는 레거시 동작.",
193
195
  "autonomous": true,
194
196
  "parallel_by_default": true
195
197
  },
@@ -19,7 +19,9 @@
19
19
  .harness/ops/
20
20
  .harness/activity/
21
21
  .harness/logs/
22
- # Migration & pre-harness backup snapshots (local-only)
22
+ # Migration / init / refresh backup snapshots (local-only — never shared)
23
+ .harness/backups/
24
+ # Completed-mission archive + sprint outputs (local runtime; goal+submission+hot-fix)
23
25
  .harness/archive/
24
26
  # Brick Office dashboard — local visualization tool + per-env data & build artifacts
25
27
  .harness/dashboard/
package/bin/init.js CHANGED
@@ -190,7 +190,11 @@ function shellQuote(value) {
190
190
  }
191
191
 
192
192
  function harnessScriptCommand(scriptName) {
193
- return `bash ${shellQuote(path.join(PROJECT_ROOT, 'scripts', scriptName))}`;
193
+ // Relative path (resolved from the project root — where Claude Code runs hooks
194
+ // and the statusLine). A relative path survives the project being moved or
195
+ // renamed; an absolute PROJECT_ROOT path bakes in the install-time location
196
+ // and breaks the moment the directory is relocated.
197
+ return `bash ${shellQuote(path.join('scripts', scriptName))}`;
194
198
  }
195
199
 
196
200
  function replaceHarnessHookCommand(entries, marker, command) {
@@ -282,7 +286,9 @@ function extractMarkdownEntryBlocks(md, prefix) {
282
286
  function migrateLegacyRoleEntries(dir, files, kind) {
283
287
  const prefix = kind === 'gotchas' ? 'G' : 'C';
284
288
  const migrated = [];
285
- const backupDir = path.join(HARNESS_DIR, 'archive', 'pre-v7-role-migration');
289
+ // Backup snapshots live under .harness/backups/, never .harness/archive/ —
290
+ // archive/ is reserved for completed-mission outputs (goal+submission+hot-fix).
291
+ const backupDir = path.join(HARNESS_DIR, 'backups', 'pre-v7-role-migration');
286
292
  for (const file of files) {
287
293
  const sourcePath = path.join(dir, file);
288
294
  if (!fs.existsSync(sourcePath)) continue;
@@ -382,8 +388,9 @@ function migrateExistingDocs() {
382
388
  continue;
383
389
  }
384
390
 
385
- // Backup
386
- const backupPath = path.join(HARNESS_DIR, 'archive', `pre-harness-${path.basename(docPath)}.bak`);
391
+ // Backup (under .harness/backups/, not archive/ — archive holds completed missions)
392
+ const backupPath = path.join(HARNESS_DIR, 'backups', `pre-harness-${path.basename(docPath)}.bak`);
393
+ ensureDir(path.dirname(backupPath));
387
394
  fs.writeFileSync(backupPath, content);
388
395
  report.push(`Backed up: ${docPath} → ${backupPath}`);
389
396
 
@@ -460,7 +467,7 @@ function migrateExistingDocs() {
460
467
  ``,
461
468
  `## Backups`,
462
469
  ``,
463
- `Original documents were preserved in \`.harness/archive/pre-harness-*.md.bak\`.`,
470
+ `Original documents were preserved in \`.harness/backups/pre-harness-*.md.bak\`.`,
464
471
  ``
465
472
  ].join('\n');
466
473
  fs.writeFileSync(reportPath, reportContent);
@@ -1421,7 +1428,7 @@ function normalizeDocBody(body) {
1421
1428
 
1422
1429
  function ensureProjectAgentDocs(opts = {}) {
1423
1430
  const dryRun = opts.dryRun || false;
1424
- const backupDir = opts.backupDir || path.join(HARNESS_DIR, 'archive', 'pre-harness-backup');
1431
+ const backupDir = opts.backupDir || path.join(HARNESS_DIR, 'backups', 'pre-harness-backup');
1425
1432
  const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
1426
1433
  const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
1427
1434
 
@@ -1481,7 +1488,7 @@ function ensureProjectAgentDocs(opts = {}) {
1481
1488
  function setupAgentsMd() {
1482
1489
  const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
1483
1490
  const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
1484
- ensureProjectAgentDocs({ backupDir: path.join(HARNESS_DIR, 'archive', 'pre-harness-backup') });
1491
+ ensureProjectAgentDocs({ backupDir: path.join(HARNESS_DIR, 'backups', 'pre-harness-backup') });
1485
1492
  }
1486
1493
 
1487
1494
  // ─────────────────────────────────────────
@@ -1638,6 +1645,7 @@ function detectMigrationNeeded() {
1638
1645
  configLegacyRouting: false,
1639
1646
  configRuntimeVerificationMissing: false,
1640
1647
  configWakeModelMissing: false,
1648
+ configWriteOnSignalMissing: false,
1641
1649
  coreSkillsStale: false,
1642
1650
  hrResourcePoolStale: false,
1643
1651
  harnessMdStale: false,
@@ -1747,6 +1755,9 @@ function detectMigrationNeeded() {
1747
1755
  if (c.company_mode && c.company_mode.hourly_wake_model === undefined) {
1748
1756
  flags.configWakeModelMissing = true;
1749
1757
  }
1758
+ if (c.company_mode && c.company_mode.write_on_signal === undefined) {
1759
+ flags.configWriteOnSignalMissing = true;
1760
+ }
1750
1761
  if (
1751
1762
  c.behavior?.auto_route_dispatcher !== undefined ||
1752
1763
  c.behavior?.auto_route_dispatcher_description ||
@@ -2031,6 +2042,10 @@ function showMigrationProposal(flags) {
2031
2042
  console.log(' • config.json: company_mode.hourly_wake_model 추가 가능');
2032
2043
  console.log(' Claude wake tick 에서 --model 을 환경변수/설정으로 지정할 수 있습니다.');
2033
2044
  }
2045
+ if (flags.configWriteOnSignalMissing) {
2046
+ console.log(' • config.json: company_mode.write_on_signal 추가 가능');
2047
+ console.log(' 무신호(델타 부재) 틱은 meeting 문서 대신 heartbeat 만 남겨 문서 폭증을 막습니다.');
2048
+ }
2034
2049
  if (flags.configLegacyRouting) {
2035
2050
  console.log(' • config.json: legacy dispatcher/conductor wording → v7 CEO/CXX wording');
2036
2051
  }
@@ -2087,7 +2102,7 @@ function showMigrationProposal(flags) {
2087
2102
  console.log(' 미리보기: npx walwal-harness migrate --dry-run');
2088
2103
  console.log('');
2089
2104
  console.log(' ※ 자동 강제 X — 사용자가 명령을 실행할 때만 변경됩니다.');
2090
- console.log(' ※ 변경 전 .harness/archive/migration-<ts>/ 에 자동 백업.');
2105
+ console.log(' ※ 변경 전 .harness/backups/migration-<ts>/ 에 자동 백업 (archive/ 는 완료 미션 전용).');
2091
2106
  console.log('');
2092
2107
  }
2093
2108
 
@@ -2114,6 +2129,7 @@ function runMigrate(opts = {}) {
2114
2129
  !flags.configLegacyRouting &&
2115
2130
  !flags.configRuntimeVerificationMissing &&
2116
2131
  !flags.configWakeModelMissing &&
2132
+ !flags.configWriteOnSignalMissing &&
2117
2133
  !flags.coreSkillsStale &&
2118
2134
  !flags.hrResourcePoolStale &&
2119
2135
  !flags.harnessMdStale &&
@@ -2140,9 +2156,20 @@ function runMigrate(opts = {}) {
2140
2156
  log(dryRun ? '=== DRY RUN — 실제 변경 없음 ===' : '=== Migration 적용 ===');
2141
2157
 
2142
2158
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
2143
- const backupDir = path.join(HARNESS_DIR, 'archive', `migration-${ts}`);
2159
+ // Migration backups go to .harness/backups/, not .harness/archive/. archive/ is
2160
+ // the completed-mission namespace (goal+submission+hot-fix); it must not collect
2161
+ // migrate snapshots. (Owner agreement: "archive/ 네임스페이스 탈환".)
2162
+ const backupDir = path.join(HARNESS_DIR, 'backups', `migration-${ts}`);
2144
2163
  if (!dryRun) ensureDir(backupDir);
2145
2164
  ensureStructuredRuntimeFiles({ dryRun });
2165
+ // Keep the host .gitignore in sync so migrating projects also ignore the new
2166
+ // .harness/backups/ snapshot root (and drop any previously-tracked runtime noise).
2167
+ if (!dryRun) {
2168
+ ensureProjectGitignore();
2169
+ gitUntrackLocalState();
2170
+ } else {
2171
+ log(' (dry-run) .gitignore: walwal-harness managed block refresh 예정 (.harness/backups/ 포함)');
2172
+ }
2146
2173
  ensureProjectAgentDocs({ dryRun, backupDir });
2147
2174
  flags.agentsMissingOpsVerificationRules = false;
2148
2175
  flags.agentsMissingCodexAdapterRules = false;
@@ -2190,7 +2217,7 @@ function runMigrate(opts = {}) {
2190
2217
  // 2. config.json — inject company_mode/runtime verification from template if missing
2191
2218
  const configPath = path.join(HARNESS_DIR, 'config.json');
2192
2219
  const tplPath = path.join(PKG_ROOT, 'assets', 'templates', 'config.json');
2193
- if ((flags.configMissingCompanyMode || flags.configLegacyRouting || flags.configRuntimeVerificationMissing || flags.configWakeModelMissing) && fs.existsSync(configPath) && fs.existsSync(tplPath)) {
2220
+ if ((flags.configMissingCompanyMode || flags.configLegacyRouting || flags.configRuntimeVerificationMissing || flags.configWakeModelMissing || flags.configWriteOnSignalMissing) && fs.existsSync(configPath) && fs.existsSync(tplPath)) {
2194
2221
  const original = fs.readFileSync(configPath, 'utf8');
2195
2222
  const c = JSON.parse(original);
2196
2223
  const tpl = JSON.parse(fs.readFileSync(tplPath, 'utf8'));
@@ -2221,6 +2248,13 @@ function runMigrate(opts = {}) {
2221
2248
  configChanged = true;
2222
2249
  log(' config.json: hourly_wake_model 섹션 동기화');
2223
2250
  }
2251
+ if (flags.configWriteOnSignalMissing) {
2252
+ c.company_mode = c.company_mode || {};
2253
+ c.company_mode.write_on_signal = tpl.company_mode.write_on_signal ?? true;
2254
+ c.company_mode.write_on_signal_description = tpl.company_mode.write_on_signal_description;
2255
+ configChanged = true;
2256
+ log(' config.json: write_on_signal (무신호 틱 문서 억제) 섹션 동기화');
2257
+ }
2224
2258
  if (flags.configRuntimeVerificationMissing && tpl.runtime?.verification) {
2225
2259
  c.runtime = c.runtime || {};
2226
2260
  c.runtime.verification = deepMerge(tpl.runtime.verification, c.runtime.verification || {});
package/init.sh CHANGED
@@ -181,13 +181,15 @@ run_refresh_ref() {
181
181
  fi
182
182
 
183
183
  local ref_path="${project_root}/.harness/ref/${role}-${stack}.md"
184
- local archive_dir="${project_root}/.harness/archive"
185
- mkdir -p "$archive_dir"
184
+ # Ref backups go to .harness/backups/, not .harness/archive/. archive/ is the
185
+ # completed-mission namespace and must not collect tooling backups.
186
+ local backups_dir="${project_root}/.harness/backups/refs"
187
+ mkdir -p "$backups_dir"
186
188
 
187
189
  if [ -f "$ref_path" ]; then
188
- local backup="${archive_dir}/ref-${role}-${stack}-$(date +%Y%m%d%H%M%S).md"
190
+ local backup="${backups_dir}/ref-${role}-${stack}-$(date +%Y%m%d%H%M%S).md"
189
191
  mv "$ref_path" "$backup"
190
- echo "archive: $backup"
192
+ echo "backup: $backup"
191
193
  fi
192
194
 
193
195
  echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.51",
3
+ "version": "7.1.53",
4
4
  "description": "Company-style AI agent harness for Claude and Codex. Installs commands, CXX agents, skills, HR-Resource hiring pool, and project-local .harness runtime state.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"
@@ -1,14 +1,23 @@
1
1
  #!/usr/bin/env bash
2
- # walwal-harness — Brick Office dashboard launcher
2
+ # walwal-harness — Brick Office dashboard launcher (shared single-install)
3
3
  #
4
- # 처음 쓰는 사용자도 한 줄로 대시보드를 띄워볼 수 있게 만든 helper.
5
- # npm 패키지에 포함된 apps/harness-dashboard 를 프로젝트별 runtime 영역
6
- # (.harness/dashboard/) 에 복사 후 dev 실행한다.
7
- # 구버전 호환을 위해 패키지에 dashboard 가 없으면 git sparse-checkout 으로 fallback 한다.
4
+ # 대시보드 코드는 모든 프로젝트에서 byte 단위로 동일하다 (패키지가 버전 고정으로
5
+ # 배포). 프로젝트마다 다른 것은 HARNESS_ROOT(어느 .harness/ 를 볼지)와 PORT 뿐이고,
6
+ # HARNESS_ROOT 는 lib/harness-root.ts 가 *런타임*에 읽는다. 따라서 설치본은 하나면
7
+ # 충분하다.
8
+ #
9
+ # 이 런처는 버전별 단일 설치본을 ~/.walwal-harness/dashboard/<version>/ 에 두고
10
+ # (npm install + next build 를 버전당 딱 한 번), 각 프로젝트는 HARNESS_ROOT+PORT 만
11
+ # 바꿔 `next start` 로 띄운다. next start 는 .next 를 읽기전용으로 쓰므로 같은 폴더에서
12
+ # 여러 프로젝트를 동시에 서빙해도 안전하다. 프로젝트가 100개여도 node_modules/.next 는
13
+ # 단 1벌(~640MB).
14
+ #
15
+ # 구버전 호환: 패키지에 dashboard 가 없으면 git sparse-checkout 으로 fallback 한다.
8
16
  #
9
17
  # Usage:
10
- # bash scripts/harness-dashboard-up.sh # install + dev (port 3001)
18
+ # bash scripts/harness-dashboard-up.sh # build(최초 1회) + serve (port 3001)
11
19
  # bash scripts/harness-dashboard-up.sh --port 3050 # override port
20
+ # bash scripts/harness-dashboard-up.sh --reinstall # 공유 설치본 폐기 후 재빌드
12
21
  # HARNESS_ROOT=/path/to/project bash ... # explicit harness root
13
22
  set -euo pipefail
14
23
 
@@ -23,12 +32,13 @@ while [[ $# -gt 0 ]]; do
23
32
  --port) PORT="$2"; shift 2 ;;
24
33
  --reinstall) REINSTALL=true; shift ;;
25
34
  --help|-h)
26
- sed -n '2,16p' "$0"
35
+ sed -n '2,28p' "$0"
27
36
  exit 0 ;;
28
37
  *) echo "[brick-office] unknown arg: $1" >&2; exit 2 ;;
29
38
  esac
30
39
  done
31
40
 
41
+ # --- target project (the one whose .harness/ we visualize) ---
32
42
  if [[ -z "${HARNESS_ROOT:-}" ]]; then
33
43
  HARNESS_ROOT="$(pwd)"
34
44
  fi
@@ -38,87 +48,91 @@ if [[ ! -d "${HARNESS_ROOT}/.harness" ]]; then
38
48
  exit 3
39
49
  fi
40
50
 
41
- LOCAL_DIR="${HARNESS_ROOT}/.harness/dashboard"
42
- VERSION_FILE="${LOCAL_DIR}/.walwal-dashboard-version"
43
-
51
+ # --- locate the packaged dashboard source + its version ---
44
52
  PACKAGE_ROOT="${HARNESS_ROOT}/node_modules/@walwal-harness/cli"
45
53
  PACKAGED_DASHBOARD="${PACKAGE_ROOT}/${DASHBOARD_PATH}"
46
54
  if [[ -f "${PACKAGE_ROOT}/package.json" ]]; then
47
55
  PACKAGE_VERSION="$(node -p "require('${PACKAGE_ROOT}/package.json').version" 2>/dev/null || echo unknown)"
48
56
  fi
49
57
 
58
+ # --- shared, version-keyed install (the whole point: one copy for ALL projects) ---
59
+ SHARED_BASE="${WALWAL_HARNESS_HOME:-$HOME/.walwal-harness}/dashboard"
60
+ VERSION_KEY="${PACKAGE_VERSION}"
61
+ [[ "$VERSION_KEY" == "unknown" || -z "$VERSION_KEY" ]] && VERSION_KEY="edge"
62
+ SHARED_DIR="${SHARED_BASE}/${VERSION_KEY}" # node_modules + .next + source live here
63
+ READY_MARKER="${SHARED_DIR}/.walwal-dashboard-ready" # written only after a successful build
64
+ LOCK_DIR="${SHARED_BASE}/.build-lock-${VERSION_KEY}" # mkdir-based build mutex (atomic)
65
+
50
66
  if [[ "$REINSTALL" == "true" ]]; then
51
- rm -rf "$LOCAL_DIR"
67
+ rm -rf "$SHARED_DIR"
52
68
  fi
53
69
 
54
- ensure_gitignore() {
55
- local gitignore="${HARNESS_ROOT}/.gitignore"
56
- # `walwal-harness init` owns the canonical managed block (covers .harness/dashboard/
57
- # plus all other runtime/local state). If it's already present, nothing to add.
58
- local managed_marker="# >>> walwal-harness managed"
59
- if [[ -f "$gitignore" ]] && grep -Fq "$managed_marker" "$gitignore"; then
60
- return
61
- fi
62
- # Prefer the package's canonical template (single source of truth shared with init.js);
63
- # fall back to a dashboard-only block for older packages that lack the template.
64
- local tpl="${PACKAGE_ROOT}/assets/templates/gitignore-append.txt"
65
- local block
66
- if [[ -f "$tpl" ]]; then
67
- block="$(cat "$tpl")"
70
+ echo "[brick-office] HARNESS_ROOT = ${HARNESS_ROOT}"
71
+ echo "[brick-office] shared install = ${SHARED_DIR} (version ${VERSION_KEY})"
72
+
73
+ # Copy the dashboard source into the shared dir. Leaves any existing node_modules
74
+ # in place (the package tarball ships no node_modules), so a re-entry after a failed
75
+ # build keeps the installed deps.
76
+ populate_source() {
77
+ mkdir -p "$SHARED_DIR"
78
+ if [[ -d "${PACKAGED_DASHBOARD}" ]]; then
79
+ cp -R "${PACKAGED_DASHBOARD}/." "${SHARED_DIR}/"
68
80
  else
69
- block="$(cat <<'EOF'
70
- # walwal-harness dashboard runtime
71
- .harness/dashboard/
72
- EOF
73
- )"
74
- fi
75
- mkdir -p "$(dirname "$gitignore")"
76
- if [[ -f "$gitignore" ]] && [[ -s "$gitignore" ]] && [[ "$(tail -c 1 "$gitignore" 2>/dev/null || true)" != "" ]]; then
77
- printf '\n' >> "$gitignore"
81
+ echo "[brick-office] 패키지에 dashboard 없음 — git sparse-checkout fallback (~5MB)."
82
+ local dl="${SHARED_BASE}/.download-${VERSION_KEY}"
83
+ rm -rf "$dl"; mkdir -p "$dl"
84
+ (
85
+ cd "$dl"
86
+ git init -q
87
+ git remote add origin "${REPO_URL}" 2>/dev/null || true
88
+ git config core.sparseCheckout true
89
+ echo "${DASHBOARD_PATH}/" > .git/info/sparse-checkout
90
+ git fetch --depth=1 origin main -q
91
+ git checkout main -q
92
+ )
93
+ cp -R "${dl}/${DASHBOARD_PATH}/." "${SHARED_DIR}/"
94
+ rm -rf "$dl"
78
95
  fi
79
- printf '%s\n' "$block" >> "$gitignore"
80
96
  }
81
97
 
82
- ensure_gitignore
98
+ # build = install deps + production build, guarded by a single-writer lock so two
99
+ # projects launching at once don't race on the same shared dir. The EXIT trap is a
100
+ # safety net: under `set -e` a failed npm install / next build aborts the script, and
101
+ # the trap then frees the lock (and leaves no READY_MARKER → next launch rebuilds).
102
+ build_shared() {
103
+ local waited=0
104
+ mkdir -p "$SHARED_BASE" # lock dir is created with plain mkdir, so its parent must exist
105
+ while ! mkdir "$LOCK_DIR" 2>/dev/null; do
106
+ # someone else finished the build while we were waiting → done, nothing to own.
107
+ [[ -f "$READY_MARKER" ]] && return 0
108
+ sleep 1; waited=$((waited + 1))
109
+ if (( waited > 600 )); then
110
+ echo "[brick-office] stale build lock (>10m) — 강제 해제." >&2
111
+ rm -rf "$LOCK_DIR"
112
+ fi
113
+ done
114
+ trap 'rm -rf "$LOCK_DIR"' EXIT
83
115
 
84
- echo "[brick-office] HARNESS_ROOT = ${HARNESS_ROOT}"
85
- echo "[brick-office] dashboard runtime path = ${LOCAL_DIR}"
86
-
87
- if [[ -d "${PACKAGED_DASHBOARD}" ]]; then
88
- INSTALLED_VERSION=""
89
- if [[ -f "${VERSION_FILE}" ]]; then
90
- INSTALLED_VERSION="$(cat "${VERSION_FILE}" 2>/dev/null || true)"
91
- fi
92
- if [[ ! -d "${LOCAL_DIR}/${DASHBOARD_PATH}" || "${INSTALLED_VERSION}" != "${PACKAGE_VERSION}" ]]; then
93
- echo "[brick-office] dashboard sync from npm package @walwal-harness/cli@${PACKAGE_VERSION}"
94
- rm -rf "${LOCAL_DIR:?}/${DASHBOARD_PATH}"
95
- mkdir -p "${LOCAL_DIR}/apps"
96
- cp -R "${PACKAGED_DASHBOARD}" "${LOCAL_DIR}/apps/"
97
- printf '%s\n' "${PACKAGE_VERSION}" > "${VERSION_FILE}"
116
+ # double-checked: another waiter may have built it just before we got the lock.
117
+ if [[ -f "$READY_MARKER" && "$(cat "$READY_MARKER" 2>/dev/null || true)" == "$VERSION_KEY" ]]; then
118
+ rm -rf "$LOCK_DIR"; trap - EXIT; return 0
98
119
  fi
99
- elif [[ ! -d "${LOCAL_DIR}/${DASHBOARD_PATH}" ]]; then
100
- echo "[brick-office] 첫 실행 — git sparse-checkout 으로 dashboard 만 가져옵니다 (~5MB)."
101
- rm -rf "${LOCAL_DIR}/.download"
102
- mkdir -p "${LOCAL_DIR}/.download"
103
- cd "${LOCAL_DIR}/.download"
104
- git init -q
105
- git remote add origin "${REPO_URL}" 2>/dev/null || true
106
- git config core.sparseCheckout true
107
- echo "${DASHBOARD_PATH}/" > .git/info/sparse-checkout
108
- git fetch --depth=1 origin main -q
109
- git checkout main -q
110
- mkdir -p "${LOCAL_DIR}/apps"
111
- rm -rf "${LOCAL_DIR:?}/${DASHBOARD_PATH}"
112
- cp -R "${DASHBOARD_PATH}" "${LOCAL_DIR}/apps/"
113
- rm -rf "${LOCAL_DIR}/.download"
114
- echo "[brick-office] 다운로드 완료."
115
- fi
116
120
 
117
- cd "${LOCAL_DIR}/${DASHBOARD_PATH}"
121
+ [[ -f "${SHARED_DIR}/package.json" ]] || populate_source
122
+
123
+ echo "[brick-office] npm install (버전당 1회, ~30s)..."
124
+ ( cd "$SHARED_DIR" && npm install --no-audit --no-fund --loglevel=error )
125
+
126
+ echo "[brick-office] next build (버전당 1회)..."
127
+ ( cd "$SHARED_DIR" && ./node_modules/.bin/next build )
128
+
129
+ printf '%s\n' "$VERSION_KEY" > "$READY_MARKER"
130
+ echo "[brick-office] 빌드 완료 — 이후 모든 프로젝트가 이 설치본을 공유합니다."
131
+ rm -rf "$LOCK_DIR"; trap - EXIT
132
+ }
118
133
 
119
- if [[ ! -d node_modules ]]; then
120
- echo "[brick-office] npm install (한 번만 실행, ~30s)..."
121
- npm install --no-audit --no-fund --loglevel=error
134
+ if [[ ! -f "$READY_MARKER" || "$(cat "$READY_MARKER" 2>/dev/null || true)" != "$VERSION_KEY" ]]; then
135
+ build_shared
122
136
  fi
123
137
 
124
138
  echo ""
@@ -130,4 +144,5 @@ echo "║ 실시간 (SSE) 시각화합니다. Ctrl+C 로 종료.
130
144
  echo "╚══════════════════════════════════════════════════════════╝"
131
145
  echo ""
132
146
 
133
- HARNESS_ROOT="${HARNESS_ROOT}" npm run dev:dashboard -- -H 127.0.0.1 -p "${PORT}"
147
+ cd "$SHARED_DIR"
148
+ HARNESS_ROOT="${HARNESS_ROOT}" exec ./node_modules/.bin/next start -H 127.0.0.1 -p "${PORT}"
@@ -40,7 +40,9 @@ meeting_rel=".harness/actions/meetings/$id/meeting-$id.md"
40
40
  meeting_path="$PROJECT_ROOT/$meeting_rel"
41
41
  log_path="$PROJECT_ROOT/.harness/progress.log"
42
42
 
43
- mkdir -p "$meeting_dir"
43
+ # meeting_dir is created lazily, only when this tick actually has a signal worth a
44
+ # meeting doc (see the write-on-signal gate below). No-signal ticks must not even
45
+ # leave an empty M-*-hourly directory behind.
44
46
 
45
47
  ops_report=""
46
48
  if [ -x "$SCRIPT_DIR/harness-service-ops-monitor.sh" ]; then
@@ -300,6 +302,49 @@ if [ "$verdict" = "working" ] && [ "$handoff_requires_execution" = "true" ]; the
300
302
  }')"
301
303
  fi
302
304
 
305
+ # ── Write-on-signal gate ────────────────────────────────────────────────────
306
+ # Agreement: 문서 ≠ 텔레메트리. A no-delta hourly tick must not emit yet another
307
+ # identical meeting doc — it bumps a heartbeat counter instead. A "signal" is an
308
+ # incident, goal drift, owner escalation, a required-execution contract, a handoff
309
+ # that needs execution, or a change in verdict since the last recorded review.
310
+ # Control flow (conductor-tick.sh) routes off progress.json state, not this file,
311
+ # so suppressing the markdown on quiet ticks is safe.
312
+ write_on_signal="$(jq -r '.company_mode.write_on_signal // true' "$CONFIG" 2>/dev/null || echo true)"
313
+ last_verdict="$(jq -r '.meetings.last_reason // ""' "$PROGRESS" 2>/dev/null || echo "")"
314
+ has_signal=true
315
+ if [ "$write_on_signal" = "true" ]; then
316
+ has_signal=false
317
+ case "$verdict" in
318
+ incident|goal_drift|owner_needed) has_signal=true ;;
319
+ esac
320
+ [ "$required_execution_json" != "null" ] && has_signal=true
321
+ [ "$handoff_requires_execution" = "true" ] && has_signal=true
322
+ # A change in verdict since the last recorded review is a state transition worth a
323
+ # minute (this also makes the first-ever review a signal, since last_verdict is "").
324
+ [ "$verdict" != "$last_verdict" ] && has_signal=true
325
+ fi
326
+
327
+ if [ "$has_signal" != "true" ]; then
328
+ # No delta — heartbeat only. Keep service-ops health fresh, leave the last real
329
+ # meeting pointer intact (read-decision fallback still resolves), and record that
330
+ # this tick was a quiet checkpoint rather than a meeting.
331
+ jq --arg ts "$ts" --arg verdict "$verdict" '
332
+ .meetings.last_reason = $verdict |
333
+ .meetings.last_review_at = $ts |
334
+ .meetings.heartbeat_count = ((.meetings.heartbeat_count // 0) + 1) |
335
+ .meetings.last_heartbeat_at = $ts |
336
+ .meetings.last_heartbeat_verdict = $verdict |
337
+ .service_ops.monitor.last_hourly_review = $ts
338
+ ' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS" || { rm -f "${PROGRESS}.tmp"; exit 1; }
339
+ hb="$(jq -r '.meetings.heartbeat_count // 0' "$PROGRESS" 2>/dev/null || echo '?')"
340
+ echo "$ts | meeting-manager | hourly-heartbeat | $verdict | no-signal (heartbeat #$hb, no meeting doc)" >> "$log_path"
341
+ # stdout: last real meeting path so the wake prompt keeps context (empty if none).
342
+ jq -r '.meetings.last_review_path // empty' "$PROGRESS" 2>/dev/null || true
343
+ exit 0
344
+ fi
345
+
346
+ mkdir -p "$meeting_dir"
347
+
303
348
  decision_json="$(jq -n \
304
349
  --arg owner "$decision_owner" \
305
350
  --arg action_type "$decision_action" \
@@ -130,7 +130,7 @@ start_headless_tick() {
130
130
  local idle="$2"
131
131
  local force="$3"
132
132
  local review_path="$4"
133
- local mode executor wake_model agent_bin stamp wake_dir ops_dir prompt_rel log_rel prompt_path log_path pid_file old_pid pid status tmux_session session_name session_safe
133
+ local mode executor wake_model agent_bin stamp ops_dir activity_dir prompt_scratch log_rel log_path pid_file old_pid pid status tmux_session session_name session_safe wake_prompt log_note
134
134
 
135
135
  if command -v jq >/dev/null 2>&1 && [ -f "$project_root/.harness/config.json" ]; then
136
136
  mode="${HARNESS_WAKE_MODE:-$(jq -r '.company_mode.hourly_wake_mode // "headless"' "$project_root/.harness/config.json" 2>/dev/null || echo headless)}"
@@ -142,13 +142,17 @@ start_headless_tick() {
142
142
  wake_model="${HARNESS_WAKE_MODEL:-}"
143
143
  fi
144
144
  stamp="$(date -u "+%Y%m%dT%H%M%SZ")"
145
- wake_dir="$project_root/.harness/actions/wake"
146
145
  ops_dir="$project_root/.harness/ops/wake"
147
- mkdir -p "$wake_dir" "$ops_dir"
148
-
149
- prompt_rel=".harness/actions/wake/wake-${stamp}.prompt.md"
146
+ activity_dir="$project_root/.harness/activity"
147
+ mkdir -p "$ops_dir" "$activity_dir"
148
+
149
+ # The wake prompt is a static template parameterized only by project/idle/force/
150
+ # review-path. Per the write-on-signal agreement we assemble it in memory rather
151
+ # than persisting a fresh wake-<ts>.prompt.md every tick (those 100+ identical
152
+ # templates were pure noise). tmux mode, which feeds the prompt over stdin, reuses
153
+ # one overwritten scratch file instead of N timestamped ones.
154
+ prompt_scratch="$ops_dir/current.prompt.md"
150
155
  log_rel=".harness/ops/wake/wake-${stamp}.log"
151
- prompt_path="$project_root/$prompt_rel"
152
156
  log_path="$project_root/$log_rel"
153
157
  pid_file="$ops_dir/wake.pid"
154
158
 
@@ -160,7 +164,7 @@ start_headless_tick() {
160
164
  fi
161
165
  fi
162
166
 
163
- cat > "$prompt_path" <<EOF
167
+ wake_prompt="$(cat <<EOF
164
168
  You are harness-ceo waking for the walwal-harness hourly executive loop.
165
169
 
166
170
  Project root: $project_root
@@ -219,7 +223,11 @@ Required final shape for this tick:
219
223
  - NEXT checkpoint:
220
224
  - Evidence paths:
221
225
  EOF
226
+ )"
222
227
 
228
+ # log_note tracks the on-disk artifact for this tick. record mode and
229
+ # BLOCKED_RUNTIME never spawn an agent, so they leave no (previously empty) log.
230
+ log_note="(none)"
223
231
  case "$mode" in
224
232
  record)
225
233
  status="recorded"
@@ -229,18 +237,19 @@ EOF
229
237
  (
230
238
  cd "$project_root" || exit 1
231
239
  if [ "$executor" = "codex" ]; then
232
- "$agent_bin" exec -C "$project_root" "$(cat "$prompt_path")" > "$log_path" 2>&1
240
+ "$agent_bin" exec -C "$project_root" "$wake_prompt" > "$log_path" 2>&1
233
241
  else
234
242
  if [ -n "$wake_model" ]; then
235
- "$agent_bin" -p "$(cat "$prompt_path")" --model "$wake_model" > "$log_path" 2>&1
243
+ "$agent_bin" -p "$wake_prompt" --model "$wake_model" > "$log_path" 2>&1
236
244
  else
237
- "$agent_bin" -p "$(cat "$prompt_path")" > "$log_path" 2>&1
245
+ "$agent_bin" -p "$wake_prompt" > "$log_path" 2>&1
238
246
  fi
239
247
  fi
240
248
  ) &
241
249
  pid=$!
242
250
  echo "$pid" > "$pid_file"
243
251
  status="spawned executor=$executor pid=$pid"
252
+ log_note="$log_rel"
244
253
  else
245
254
  status="BLOCKED_RUNTIME ${executor}-not-found"
246
255
  fi
@@ -249,18 +258,20 @@ EOF
249
258
  if ! command -v tmux >/dev/null 2>&1; then
250
259
  status="BLOCKED_RUNTIME tmux-not-found"
251
260
  elif agent_bin="$(resolve_agent_bin "$executor")"; then
261
+ printf '%s\n' "$wake_prompt" > "$prompt_scratch"
252
262
  session_safe="$(basename "$project_root" | tr -c '[:alnum:]_' '_')"
253
263
  session_name="walwal_${session_safe}_${stamp}"
254
264
  if [ "$executor" = "codex" ]; then
255
- tmux new-session -d -s "$session_name" "cd '$project_root' && '$agent_bin' exec -C '$project_root' - < '$prompt_path' 2>&1 | tee '$log_path'"
265
+ tmux new-session -d -s "$session_name" "cd '$project_root' && '$agent_bin' exec -C '$project_root' - < '$prompt_scratch' 2>&1 | tee '$log_path'"
256
266
  else
257
267
  if [ -n "$wake_model" ]; then
258
- tmux new-session -d -s "$session_name" "cd '$project_root' && '$agent_bin' -p \"\$(cat '$prompt_path')\" --model '$wake_model' 2>&1 | tee '$log_path'"
268
+ tmux new-session -d -s "$session_name" "cd '$project_root' && '$agent_bin' -p \"\$(cat '$prompt_scratch')\" --model '$wake_model' 2>&1 | tee '$log_path'"
259
269
  else
260
- tmux new-session -d -s "$session_name" "cd '$project_root' && '$agent_bin' -p \"\$(cat '$prompt_path')\" 2>&1 | tee '$log_path'"
270
+ tmux new-session -d -s "$session_name" "cd '$project_root' && '$agent_bin' -p \"\$(cat '$prompt_scratch')\" 2>&1 | tee '$log_path'"
261
271
  fi
262
272
  fi
263
273
  status="spawned-tmux executor=$executor session=$session_name"
274
+ log_note="$log_rel"
264
275
  else
265
276
  status="BLOCKED_RUNTIME ${executor}-not-found"
266
277
  fi
@@ -270,13 +281,41 @@ EOF
270
281
  ;;
271
282
  esac
272
283
 
273
- echo "$(date -u "+%Y-%m-%dT%H:%M:%SZ") | wake | agent-tick | $status | idle=${idle}s | force=${force} | mode=${mode} | prompt=$prompt_rel | log=$log_rel" >> "$project_root/.harness/progress.log"
274
- say "wake-agent: $project_root (idle ${idle}s, force=${force}, mode=${mode}, executor=${executor}) → $status $log_rel"
284
+ # Per-tick wake telemetry = one params-only line, not a full prompt copy. This
285
+ # is the "if you must keep something, keep one line in activity.jsonl" path from
286
+ # the wake-template agreement, with rotation so it never grows unbounded.
287
+ if command -v jq >/dev/null 2>&1; then
288
+ jq -nc \
289
+ --arg ts "$(date -u "+%Y-%m-%dT%H:%M:%SZ")" \
290
+ --arg stamp "$stamp" \
291
+ --argjson idle "${idle:-0}" \
292
+ --arg force "$force" \
293
+ --arg mode "$mode" \
294
+ --arg executor "$executor" \
295
+ --arg status "$status" \
296
+ --arg review "${review_path:-}" \
297
+ --arg log "$log_note" \
298
+ '{ts:$ts,stamp:$stamp,idle:$idle,force:$force,mode:$mode,executor:$executor,status:$status,review:$review,log:$log}' \
299
+ >> "$activity_dir/wake.jsonl" 2>/dev/null || true
300
+ if [ -f "$activity_dir/wake.jsonl" ] && tail -n 2000 "$activity_dir/wake.jsonl" > "$activity_dir/wake.jsonl.tmp" 2>/dev/null; then
301
+ mv "$activity_dir/wake.jsonl.tmp" "$activity_dir/wake.jsonl"
302
+ else
303
+ rm -f "$activity_dir/wake.jsonl.tmp" 2>/dev/null || true
304
+ fi
305
+ fi
306
+
307
+ # Rotate per-tick agent logs: keep only the most recent 48 (~2 days hourly).
308
+ ls -1t "$ops_dir"/wake-*.log 2>/dev/null | tail -n +49 | while IFS= read -r old_log; do
309
+ rm -f "$old_log"
310
+ done
311
+
312
+ echo "$(date -u "+%Y-%m-%dT%H:%M:%SZ") | wake | agent-tick | $status | idle=${idle}s | force=${force} | mode=${mode} | log=$log_note" >> "$project_root/.harness/progress.log"
313
+ say "wake-agent: $project_root (idle ${idle}s, force=${force}, mode=${mode}, executor=${executor}) → $status $log_note"
275
314
 
276
315
  run_conductor_fallback "$project_root" "$status"
277
316
 
278
317
  if tmux_session="$(find_tmux_session "$project_root" 2>/dev/null)"; then
279
- tmux display-message -t "$tmux_session" "walwal wake headless tick: $status ($log_rel)" 2>/dev/null || true
318
+ tmux display-message -t "$tmux_session" "walwal wake headless tick: $status ($log_note)" 2>/dev/null || true
280
319
  fi
281
320
  }
282
321
 
@@ -8,7 +8,8 @@ SCAN_RESULT="${PROJECT_ROOT}/.harness/actions/scan-result.json"
8
8
  export SCAN_RESULT PROJECT_ROOT
9
9
  AGENTS_FILE="${PROJECT_ROOT}/AGENTS.md"
10
10
  CLAUDE_FILE="${PROJECT_ROOT}/CLAUDE.md"
11
- BACKUP_DIR="${PROJECT_ROOT}/.harness/archive/pre-harness-backup"
11
+ # Backups live under .harness/backups/, not archive/ (archive = completed missions).
12
+ BACKUP_DIR="${PROJECT_ROOT}/.harness/backups/pre-harness-backup"
12
13
 
13
14
  if [ ! -f "$SCAN_RESULT" ]; then
14
15
  echo "ERROR: scan-result.json not found. Run scan-project.sh first."
@@ -44,8 +44,9 @@ done
44
44
  SCAN="${PROJECT_ROOT}/.harness/actions/scan-result.json"
45
45
  REF_DIR="${PROJECT_ROOT}/.harness/ref"
46
46
  META="${REF_DIR}/.generated.json"
47
- ARCHIVE_DIR="${PROJECT_ROOT}/.harness/archive"
48
- mkdir -p "$REF_DIR" "$ARCHIVE_DIR"
47
+ # Ref backups go to .harness/backups/refs, not archive/ (archive = completed missions).
48
+ BACKUP_DIR="${PROJECT_ROOT}/.harness/backups/refs"
49
+ mkdir -p "$REF_DIR" "$BACKUP_DIR"
49
50
  [ -f "$META" ] || echo "{}" > "$META"
50
51
 
51
52
  # ───── 감지된 스택 목록 ─────
@@ -192,9 +193,9 @@ process_one() {
192
193
  return 0
193
194
  fi
194
195
  if [ -f "$path" ] && $REFRESH; then
195
- local backup="${ARCHIVE_DIR}/ref-${role}-${stack}-$(date +%Y%m%d%H%M%S).md"
196
+ local backup="${BACKUP_DIR}/ref-${role}-${stack}-$(date +%Y%m%d%H%M%S).md"
196
197
  cp "$path" "$backup"
197
- echo "archived: $backup"
198
+ echo "backup: $backup"
198
199
  fi
199
200
 
200
201
  local answer="y"