@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 +8 -1
- package/assets/templates/config.json +2 -0
- package/assets/templates/gitignore-append.txt +3 -1
- package/bin/init.js +39 -9
- package/init.sh +6 -4
- package/package.json +1 -1
- package/scripts/harness-hourly-review.sh +46 -1
- package/scripts/harness-wake.sh +55 -16
- package/scripts/init-agents-md.sh +2 -1
- package/scripts/init-ref-docs.sh +5 -4
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-
|
|
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
|
|
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
|
-
|
|
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, '
|
|
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/
|
|
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, '
|
|
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, '
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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="${
|
|
190
|
+
local backup="${backups_dir}/ref-${role}-${stack}-$(date +%Y%m%d%H%M%S).md"
|
|
189
191
|
mv "$ref_path" "$backup"
|
|
190
|
-
echo "
|
|
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.
|
|
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
|
-
|
|
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" \
|
package/scripts/harness-wake.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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" "$
|
|
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 "$
|
|
243
|
+
"$agent_bin" -p "$wake_prompt" --model "$wake_model" > "$log_path" 2>&1
|
|
236
244
|
else
|
|
237
|
-
"$agent_bin" -p "$
|
|
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' - < '$
|
|
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 '$
|
|
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 '$
|
|
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
|
-
|
|
274
|
-
|
|
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 ($
|
|
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
|
-
|
|
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."
|
package/scripts/init-ref-docs.sh
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
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="${
|
|
196
|
+
local backup="${BACKUP_DIR}/ref-${role}-${stack}-$(date +%Y%m%d%H%M%S).md"
|
|
196
197
|
cp "$path" "$backup"
|
|
197
|
-
echo "
|
|
198
|
+
echo "backup: $backup"
|
|
198
199
|
fi
|
|
199
200
|
|
|
200
201
|
local answer="y"
|