@walwal-harness/cli 7.1.52 → 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,13 @@ 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
+
34
41
  ## 7.1.52 — Single shared dashboard install (drop per-project `node_modules`) (2026-06-23)
35
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.
36
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.
@@ -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
@@ -286,7 +286,9 @@ function extractMarkdownEntryBlocks(md, prefix) {
286
286
  function migrateLegacyRoleEntries(dir, files, kind) {
287
287
  const prefix = kind === 'gotchas' ? 'G' : 'C';
288
288
  const migrated = [];
289
- 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');
290
292
  for (const file of files) {
291
293
  const sourcePath = path.join(dir, file);
292
294
  if (!fs.existsSync(sourcePath)) continue;
@@ -386,8 +388,9 @@ function migrateExistingDocs() {
386
388
  continue;
387
389
  }
388
390
 
389
- // Backup
390
- 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));
391
394
  fs.writeFileSync(backupPath, content);
392
395
  report.push(`Backed up: ${docPath} → ${backupPath}`);
393
396
 
@@ -464,7 +467,7 @@ function migrateExistingDocs() {
464
467
  ``,
465
468
  `## Backups`,
466
469
  ``,
467
- `Original documents were preserved in \`.harness/archive/pre-harness-*.md.bak\`.`,
470
+ `Original documents were preserved in \`.harness/backups/pre-harness-*.md.bak\`.`,
468
471
  ``
469
472
  ].join('\n');
470
473
  fs.writeFileSync(reportPath, reportContent);
@@ -1425,7 +1428,7 @@ function normalizeDocBody(body) {
1425
1428
 
1426
1429
  function ensureProjectAgentDocs(opts = {}) {
1427
1430
  const dryRun = opts.dryRun || false;
1428
- 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');
1429
1432
  const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
1430
1433
  const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
1431
1434
 
@@ -1485,7 +1488,7 @@ function ensureProjectAgentDocs(opts = {}) {
1485
1488
  function setupAgentsMd() {
1486
1489
  const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
1487
1490
  const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
1488
- ensureProjectAgentDocs({ backupDir: path.join(HARNESS_DIR, 'archive', 'pre-harness-backup') });
1491
+ ensureProjectAgentDocs({ backupDir: path.join(HARNESS_DIR, 'backups', 'pre-harness-backup') });
1489
1492
  }
1490
1493
 
1491
1494
  // ─────────────────────────────────────────
@@ -1642,6 +1645,7 @@ function detectMigrationNeeded() {
1642
1645
  configLegacyRouting: false,
1643
1646
  configRuntimeVerificationMissing: false,
1644
1647
  configWakeModelMissing: false,
1648
+ configWriteOnSignalMissing: false,
1645
1649
  coreSkillsStale: false,
1646
1650
  hrResourcePoolStale: false,
1647
1651
  harnessMdStale: false,
@@ -1751,6 +1755,9 @@ function detectMigrationNeeded() {
1751
1755
  if (c.company_mode && c.company_mode.hourly_wake_model === undefined) {
1752
1756
  flags.configWakeModelMissing = true;
1753
1757
  }
1758
+ if (c.company_mode && c.company_mode.write_on_signal === undefined) {
1759
+ flags.configWriteOnSignalMissing = true;
1760
+ }
1754
1761
  if (
1755
1762
  c.behavior?.auto_route_dispatcher !== undefined ||
1756
1763
  c.behavior?.auto_route_dispatcher_description ||
@@ -2035,6 +2042,10 @@ function showMigrationProposal(flags) {
2035
2042
  console.log(' • config.json: company_mode.hourly_wake_model 추가 가능');
2036
2043
  console.log(' Claude wake tick 에서 --model 을 환경변수/설정으로 지정할 수 있습니다.');
2037
2044
  }
2045
+ if (flags.configWriteOnSignalMissing) {
2046
+ console.log(' • config.json: company_mode.write_on_signal 추가 가능');
2047
+ console.log(' 무신호(델타 부재) 틱은 meeting 문서 대신 heartbeat 만 남겨 문서 폭증을 막습니다.');
2048
+ }
2038
2049
  if (flags.configLegacyRouting) {
2039
2050
  console.log(' • config.json: legacy dispatcher/conductor wording → v7 CEO/CXX wording');
2040
2051
  }
@@ -2091,7 +2102,7 @@ function showMigrationProposal(flags) {
2091
2102
  console.log(' 미리보기: npx walwal-harness migrate --dry-run');
2092
2103
  console.log('');
2093
2104
  console.log(' ※ 자동 강제 X — 사용자가 명령을 실행할 때만 변경됩니다.');
2094
- console.log(' ※ 변경 전 .harness/archive/migration-<ts>/ 에 자동 백업.');
2105
+ console.log(' ※ 변경 전 .harness/backups/migration-<ts>/ 에 자동 백업 (archive/ 는 완료 미션 전용).');
2095
2106
  console.log('');
2096
2107
  }
2097
2108
 
@@ -2118,6 +2129,7 @@ function runMigrate(opts = {}) {
2118
2129
  !flags.configLegacyRouting &&
2119
2130
  !flags.configRuntimeVerificationMissing &&
2120
2131
  !flags.configWakeModelMissing &&
2132
+ !flags.configWriteOnSignalMissing &&
2121
2133
  !flags.coreSkillsStale &&
2122
2134
  !flags.hrResourcePoolStale &&
2123
2135
  !flags.harnessMdStale &&
@@ -2144,9 +2156,20 @@ function runMigrate(opts = {}) {
2144
2156
  log(dryRun ? '=== DRY RUN — 실제 변경 없음 ===' : '=== Migration 적용 ===');
2145
2157
 
2146
2158
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
2147
- 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}`);
2148
2163
  if (!dryRun) ensureDir(backupDir);
2149
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
+ }
2150
2173
  ensureProjectAgentDocs({ dryRun, backupDir });
2151
2174
  flags.agentsMissingOpsVerificationRules = false;
2152
2175
  flags.agentsMissingCodexAdapterRules = false;
@@ -2194,7 +2217,7 @@ function runMigrate(opts = {}) {
2194
2217
  // 2. config.json — inject company_mode/runtime verification from template if missing
2195
2218
  const configPath = path.join(HARNESS_DIR, 'config.json');
2196
2219
  const tplPath = path.join(PKG_ROOT, 'assets', 'templates', 'config.json');
2197
- 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)) {
2198
2221
  const original = fs.readFileSync(configPath, 'utf8');
2199
2222
  const c = JSON.parse(original);
2200
2223
  const tpl = JSON.parse(fs.readFileSync(tplPath, 'utf8'));
@@ -2225,6 +2248,13 @@ function runMigrate(opts = {}) {
2225
2248
  configChanged = true;
2226
2249
  log(' config.json: hourly_wake_model 섹션 동기화');
2227
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
+ }
2228
2258
  if (flags.configRuntimeVerificationMissing && tpl.runtime?.verification) {
2229
2259
  c.runtime = c.runtime || {};
2230
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.52",
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"
@@ -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"