diff --git a/.agents/reports/planner-reviewer-claude-01/report-b368210a.md b/.agents/reports/planner-reviewer-claude-01/report-b368210a.md new file mode 100644 index 0000000..096c18e --- /dev/null +++ b/.agents/reports/planner-reviewer-claude-01/report-b368210a.md @@ -0,0 +1,144 @@ +# ๐Ÿ” Review Report โ€” Job b368210a (branch `fix/issue-12-8-7` 2ํšŒ์ฐจ, HEAD `ea911d7`) + +- **Reviewer**: `planner-reviewer-claude-01` (role: planner,reviewer) ยท 2026-09-13 +- **๋Œ€์ƒ**: `git log 8a09e02..HEAD` ์˜ ๋„ค ์ปค๋ฐ‹. 1ํšŒ์ฐจ์—์„œ PASS ํ•œ ๋ถ€๋ถ„์€ ํšŒ๊ท€ ์ค‘์‹ฌ์œผ๋กœ, ์ƒˆ ์ปค๋ฐ‹ 2๊ฑด์€ ์ •๋ฐ€ ๊ฒ€ํ† ํ–ˆ๋‹ค. + - `e629cc1`: fix #12ยท#8ยท#7 + - `4176d75`: release v4.4.4 + - **`0fedae2`**: N1 ์ˆ˜์ • โ€” ์ƒˆ ์ปค๋ฐ‹ + - **`ea911d7`**: ๋น„์ฐจ๋‹จ ๊ถŒ๊ณ  ๋ฐ˜์˜ โ€” ์ƒˆ ์ปค๋ฐ‹ +- **HEAD ๊ณ ์ •**: ๋ฆฌ๋ทฐ ์‹œ์ž‘(12:31)๋ถ€ํ„ฐ ๋๊นŒ์ง€ HEAD ๋Š” `ea911d7`, ์ž‘์—… ํŠธ๋ฆฌ๋Š” ๊นจ๋—ํ–ˆ๋‹ค. ๊ฐ™์€ HEAD ์— ๋Œ€ํ•œ ๊ณ ์•„ ์žก `0cf8d1c0` ์˜ PASS ํŒ๋‹จ์„ ์ด ์žก์—์„œ **์ƒˆ ์ฆ๊ฑฐ๋กœ ๋‹ค์‹œ ํ™•์ธ**ํ–ˆ๋‹ค(์ƒŒ๋“œ๋ฐ•์Šคยทํ…Œ์ŠคํŠธ ๋ชจ๋‘ ์ด ์žก์—์„œ ์žฌ์‹คํ–‰). +- **๋ฐฉ๋ฒ•**: + - ์ฝ”๋“œ ์ •๋… + - `$TMPDIR` ์ƒŒ๋“œ๋ฐ•์Šค ์žฌํ˜„(๋ชจ๋‘ ๋๋‚˜๊ณ  ์‚ญ์ œ) + - `scrub-secrets` ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ + - `update.sh` ๋ณ‘ํ•ฉ ๋ธ”๋ก ์ด์ „/์ดํ›„ ๋น„๊ต + - `git archive` ์‚ฌ๋ณธ์—์„œ #7 ํ…Œ์ŠคํŠธ ๋ณ€์ด ๊ฒ€์‚ฌ + - ์‹ค์ œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋Š” **์ฝ๊ธฐ ์ „์šฉ** ์ง‘๊ณ„๋งŒ ํ–ˆ๋‹ค(`scrub-secrets` ๋Š” ์‹ค์ œ `.mam/jobs` ์— `--dry-run` ์œผ๋กœ๋„ ์‹คํ–‰ํ•˜์ง€ ์•Š์Œ) + - ๋ธŒ๋ฆฌํ”„ ์ง€์ • ํ…Œ์ŠคํŠธ(์‹ค์ œ `.mam/jobs` ์Šค๋ƒ…์ƒท ๋น„๊ต) + - ์ •์  ๊ฒ€์‚ฌ + - ๋ฆฌ๋ทฐ ์ค‘ 45์ดˆ ๊ฐ„๊ฒฉ `progress` heartbeat ๋ฐœํ–‰. ์ฝ”๋“œ ์ˆ˜์ • ์—†์Œ. + +## 0. 1ํšŒ์ฐจ ๊ฒฐ๊ณผ์™€ ์ฒ˜๋ฆฌ (๋ธŒ๋ฆฌํ”„ ๊ธฐ์ค€) + +| ๋ฆฌ๋ทฐ์–ด | 1ํšŒ์ฐจ(`4176d75`) ํŒ์ • | +|---|---| +| claude | **NOT PASS** โ€” N1: `scrub-secrets` ๊ฐ€ ํ‚ค์˜ ์กด์žฌ๋กœ ๋Œ€์ƒ์„ ๊ณจ๋ผ null ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ ˆ์ฝ”๋“œ 901๊ฑด์„ ์žฌ๊ธฐ๋ก | +| agy | PASS | +| grok | PASS | +| codex | ์‚ฌ์šฉ๋Ÿ‰ ํ•œ๋„๋กœ ๋ฆฌ๋ทฐ ๋ถˆ๊ฐ€(์ด๋ฒˆ ํšŒ์ฐจ ์ œ์™ธ, ํ•œ๋„ ๋ณต๊ตฌ ํ›„ ๋ณ„๋„ ๋ฆฌ๋ทฐ ์˜ˆ์ •) | + +creator ๋Š” N1 ์„ `0fedae2` ๋กœ ๊ณ ์ณค๊ณ , ๋น„์ฐจ๋‹จ ๊ถŒ๊ณ  1ยท2๋ฒˆ(`update.sh` ๋ณ‘ํ•ฉ ๊ฒฌ๊ณ ํ™”, #7 ํ…Œ์ŠคํŠธ ๋ณด๊ฐ•)์„ `ea911d7` ๋กœ ๋ฐ˜์˜ํ–ˆ๋‹ค. + +## 1. `0fedae2` โ€” `scrub-secrets` ์„ ํƒ ์กฐ๊ฑด โ€” โœ… + +### 1-1. ์ฝ”๋“œยท๋ฌธ์„œ +- `registry.py` `_cmd_scrub_secrets` ์˜ ์„ ํƒ ์กฐ๊ฑด์ด `"password" in broker` โ†’ **`broker.get("password")`** ๋กœ ๋ฐ”๋€Œ์—ˆ๋‹ค. ์ €์žฅ๋œ ๊ฐ’์ด ์žˆ๋Š” ๋ ˆ์ฝ”๋“œ๋งŒ ๋Œ€์ƒ์ด๋‹ค. +- `registry_lock` ์•„๋ž˜์—์„œ ๋™์ž‘ํ•˜๊ณ , `--dry-run` ์ด๋ฉด ๊ธฐ๋กํ•˜์ง€ ์•Š์œผ๋ฉฐ, ๊ธฐ๋ก์€ `_atomic_write_record` ๋ฅผ ๊ฑฐ์นœ๋‹ค. +- ์„œ์ˆ ๋„ ์ฝ”๋“œ์™€ ์ผ์น˜ํ•œ๋‹ค. + - `registry.md:60`: "cleaned on their next write; `registry.py scrub-secrets [--dry-run]` strips โ€ฆ" + - `VERSIONS.md:46`: "๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ’์ด ์‹ค์ œ๋กœ ์ €์žฅ๋œ ๋ ˆ์ฝ”๋“œ๋งŒ โ€ฆ null ํ‚ค๋Š” โ€ฆ ๋Œ€์ƒ๋„ ๋ณด๊ณ ๋„ ์•„๋‹ˆ๋ฉฐ, ๋‹ค์Œ ์“ฐ๊ธฐ ๋•Œ `_atomic_write_record` ๊ฐ€ ์ •๋ฆฌ" +- `registry.py` ๋Š” `0fedae2` ์ดํ›„ ๋ฌด๋ณ€๊ฒฝ์ด๋‹ค(`git diff --stat 0fedae2..HEAD` ๋Š” `update.sh`ยท#7 ํ…Œ์ŠคํŠธ๋ฟ). + +### 1-2. ์ƒŒ๋“œ๋ฐ•์Šค ์žฌํ˜„ (์ด ์žก์—์„œ ์ƒˆ๋กœ ์‹คํ–‰) +- ๋ ˆ์ฝ”๋“œ ๋„ค ๊ฐœ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค: `password: None` / `""` / ๊ฐ€์งœ ๊ฐ’(uuid4) / ํ‚ค ์—†์Œ. ๋ชจ๋‘ `host/port/tls/username`ยท`prompt` ๋ฅผ ๊ฐ€์ง„๋‹ค. +- ๋Œ€์ƒ ์•ˆ์ „์žฅ์น˜: ์ตœ์ƒ์œ„ `--registry-dir`(`registry.py:270`, ์„œ๋ธŒํŒŒ์„œ๋Š” `--dry-run` ๋งŒ ์ •์˜)์— ๋”ํ•ด `MAM_JOBS_DIR` ๋„ ์ƒŒ๋“œ๋ฐ•์Šค๋กœ ์ง€์ •ํ–ˆ๋‹ค. dry-run ์ถœ๋ ฅ์ด ์ƒŒ๋“œ๋ฐ•์Šค ๊ฒฝ๋กœ๋ฅผ ๊ฐ€๋ฆฌํ‚ฌ ๋•Œ๋งŒ ์‹ค์ œ ์‹คํ–‰์œผ๋กœ ๋„˜์–ด๊ฐ€๊ฒŒ ํ–ˆ๋‹ค. + +| ๋‹จ๊ณ„ | ๊ฒฐ๊ณผ | +|---|---| +| `--dry-run` | rc 0, "would scrub **1** record(s) under ``: **k_real**", ๋„ค ํŒŒ์ผ ๋ชจ๋‘ ๋ฐ”์ดํŠธ ๋™์ผ | +| ์‹ค์ œ ์‹คํ–‰ | rc 0, "scrubbed 1 record(s) under ``: k_real", ๋ฐ”๋€ ํŒŒ์ผ์€ `k_real` ํ•˜๋‚˜ | +| ์‹คํ–‰ ํ›„ | `k_real` ๋งŒ `password` ํ‚ค ์ œ๊ฑฐ. `k_null`ยท`k_empty` ๋Š” ํ‚ค ์œ ์ง€(๋‹ค์Œ ์“ฐ๊ธฐ์— ๋งก๊น€), `k_absent` ๋ฌด๋ณ€๊ฒฝ. ๋„ค ๋ ˆ์ฝ”๋“œ ๋ชจ๋‘ ๋‚˜๋จธ์ง€ broker ํ•„๋“œ์™€ `prompt` ๋ณด์กด | + +### 1-3. ์‹ค์ œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ (์ฝ๊ธฐ ์ „์šฉ ์ง‘๊ณ„, ๋ช…๋ น ๋ฏธ์‹คํ–‰) +910๊ฑด ์ค‘ ์ƒˆ ๊ทœ์น™์œผ๋กœ ์„ ํƒ๋˜๋Š” ๋ ˆ์ฝ”๋“œ๋Š” **0๊ฑด**์ด๋‹ค. null/๋นˆ password ํ‚ค๋ฅผ ๊ฐ€์ง„ 901๊ฑด์€ ์„ ํƒ๋˜์ง€ ์•Š๋Š”๋‹ค. + +### 1-4. ํšŒ๊ท€ ํ…Œ์ŠคํŠธ +`test_scrub_secrets_cli_migrates_a_registry` ๊ฐ€ ๋ ˆ๊ฑฐ์‹œ `password: null` ๋ ˆ์ฝ”๋“œ๋ฅผ ํฌํ•จํ•œ๋‹ค. dry-run ๋ชฉ๋ก์— ์—†๊ณ , ์‹ค์ œ ์‹คํ–‰ ํ›„์—๋„ ๋ฐ”์ดํŠธ ๋‹จ์œ„๋กœ ๋™์ผํ•จ์„ ๋‹จ์ •ํ•œ๋‹ค. + +## 2. `ea911d7` โ€” `deploy/update.sh:239-257` `install_state` ๋ณ‘ํ•ฉ โ€” โœ… + +**์ฝ”๋“œ** +- **๋ ๊ฐœํ–‰ ๋ณด์žฅ**(241-245): `[ -s โ€ฆ ] && [ -n "$(tail -c1 โ€ฆ)" ]`. + - ๋ช…๋ น ์น˜ํ™˜์ด ๋ ๊ฐœํ–‰์„ ์ง€์šฐ๋ฏ€๋กœ, ๋งˆ์ง€๋ง‰ ๋ฐ”์ดํŠธ๊ฐ€ ๊ฐœํ–‰์ด ์•„๋‹ ๋•Œ๋งŒ `\n` ์„ ๋ถ™์ธ๋‹ค. ๋นˆ ํŒŒ์ผ์€ ๊ฑด๋„ˆ๋›ด๋‹ค. +- **`=` ์—†๋Š” ์ค„ ๋ฌด์‹œ**(247-250): `case "$line" in *=*)`. +- **์ •ํ™• ํ‚ค ๋งค์นญ**(254): `awk -F= -v k="$key" '$1 == k โ€ฆ'`. + - `key="${line%%=*}"` ์™€ awk `$1` ์€ ๋‘˜ ๋‹ค ์ฒซ `=` ์•ž๋ถ€๋ถ„์ด๋ผ ๊ธฐ์ค€์ด ๊ฐ™๋‹ค. + - `if !` ์กฐ๊ฑด ์•ˆ์ด๋ผ `update.sh:8` ์˜ `set -euo pipefail` ์•„๋ž˜์—์„œ๋„ ์•ˆ์ „ํ•˜๋‹ค. +- ์‹คํŒจ ๊ฒฝ๋กœ `restore_on_failure` ๋Š” ๋ฌด๋ณ€๊ฒฝ์ด๋‹ค. + +**์ƒŒ๋“œ๋ฐ•์Šค ๋น„๊ต**(์ด ์žก์—์„œ ์ƒˆ๋กœ ์‹คํ–‰): HEAD ์™€ `0fedae2` ์˜ ๋ธ”๋ก์„ ๊ทธ๋Œ€๋กœ ์ถ”์ถœํ•ด, ๊ฐ™์€ ์ž…๋ ฅ์„ `bash -euo pipefail` ๋กœ ์‹คํ–‰ํ–ˆ๋‹ค. +- ์ƒˆ ํŒŒ์ผ: `axb=new`, `version=2`(๋ ๊ฐœํ–‰ ์—†์Œ) +- ์ด์ „ ํŒŒ์ผ: `a.b=old`, `axb=old`, `gitignore_created=1`, `# comment`, `noequals`, `=emptykey`, `k=v=w`, `version=1`(๋ ๊ฐœํ–‰ ์—†์Œ) + +| ๋ธ”๋ก | ๊ฒฐ๊ณผ | +|---|---| +| `0fedae2` | ๋‘ ํ‚ค๊ฐ€ ํ•œ ์ค„๋กœ ๋ถ™์–ด `version=2gitignore_created=1` ์ด ๋จ. `a.b=old` ์œ ์‹ค(์ •๊ทœ์‹ `^a.b=` ๊ฐ€ `axb=` ์— ์ผ์น˜). `# comment`ยท`noequals` ์ถ”๊ฐ€ | +| `ea911d7` | `axb=new` / `version=2` / `a.b=old` / `gitignore_created=1` / `k=v=w` โ€” ๊ธฐ๋Œ€์™€ ์ผ์น˜, rc 0 | +| `ea911d7` ์ถ”๊ฐ€ ํ™•์ธ | ๋‘ ๋ฒˆ์งธ ์‹คํ–‰ ๊ฒฐ๊ณผ ๋™์ผ(๋ฉฑ๋“ฑ). ์ƒˆ ํŒŒ์ผ์ด ๋นˆ ๊ฒฝ์šฐยท์—†๋Š” ๊ฒฝ์šฐ ๋ชจ๋‘ `gitignore_created=1` ํ•œ ์ค„ | + +**์ฐธ๊ณ (๋ฌดํ•ด)**: `awk -v` ๋Š” ๊ฐ’์˜ ์—ญ์Šฌ๋ž˜์‹œ ์ด์Šค์ผ€์ดํ”„๋ฅผ ํ•ด์„ํ•œ๋‹ค. ํ•˜์ง€๋งŒ `install_state` ํ‚ค๋Š” `install.sh:597,601` ์˜ `gitignore_created` ๋ฟ์ด๋ผ ์˜ํ–ฅ์ด ์—†๋‹ค. + +`test_issue8_update_state.py`(์‹ค์ œ installโ†’update ์‚ฌ์ดํด), `test_deploy_layout.py`, `test_deploy_freshness.py` ๋ชจ๋‘ ํ†ต๊ณผํ–ˆ๋‹ค(ยง5). + +## 3. `ea911d7` โ€” #7 ํ…Œ์ŠคํŠธ 2๊ฑด์ด ์‹ค์ œ ์ฝ”๋“œ ๊ฒฝ๋กœ๋ฅผ ๊ฒ€์ฆํ•˜๋Š”๊ฐ€ โ€” โœ… + +### 3-1. ์ฝ”๋“œ ๊ฒฝ๋กœ +**`test_idle_pane_in_a_later_workspace_is_reused`** +- ์ƒํ™ฉ: w1 ์€ ๊ฐ€๋“ ์ฐผ๊ณ (4), w2 ์— agent ์—†๋Š” pane `w2:p_idle` ์ด ์žˆ๋‹ค. +- `lib.sh` ๊ฐ€ ๋”ฐ๋ผ๊ฐ€๋Š” ๊ฒฝ๋กœ: + 1. `lib.sh:764-777`: ํ›„๋ณด๋งˆ๋‹ค idle ํ•„ํ„ฐ `not p.get('agent')` ๋ฅผ ์ ์šฉํ•œ๋‹ค. w1 ์—์„œ๋Š” ์ฐพ์ง€ ๋ชปํ•œ๋‹ค. + 2. `lib.sh:812-815`: layout ์ด overflow ์ด๋ฏ€๋กœ `continue`. + 3. `lib.sh:778-786`: w2 ์˜ idle pane ์„ ์„ ํƒํ•˜๊ณ  `break`. +- ๋‹จ์ •: splitยทcreate ๊ฐ€ ์—†๊ณ , `agent start` ๋Œ€์ƒ์ด `w2:p_idle` ์ด๋‹ค. + +**`test_ws_label_rename_targets_only_the_chosen_workspace`** +- ๋‹จ์ •: rename ํ˜ธ์ถœ์ด ์ •ํ™•ํžˆ `["workspace","rename","w2","team-b"]` ํ•˜๋‚˜๋‹ค(`lib.sh:854-856`). + +`lib.sh` ๋Š” `e629cc1` ์ดํ›„ ๋ฌด๋ณ€๊ฒฝ์ด๋‹ค. + +### 3-2. ๋ณ€์ด ๊ฒ€์‚ฌ (`git archive HEAD` ์‚ฌ๋ณธ, ์ €์žฅ์†Œ ๋ฌด๋ณ€๊ฒฝ) +- `conftest.py:22` ๊ฐ€ ์ž๊ธฐ ์œ„์น˜ ๊ธฐ์ค€์œผ๋กœ `.agents/skills` ๋ฅผ ๋ณต์‚ฌํ•˜๋ฏ€๋กœ, ์‚ฌ๋ณธ์˜ ํ…Œ์ŠคํŠธ๋Š” ์‚ฌ๋ณธ์˜ `lib.sh` ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. + +| ์‚ฌ๋ณธ | ๋ณ€์ด | idle ์žฌ์‚ฌ์šฉ ํ…Œ์ŠคํŠธ | rename ๋Œ€์ƒ ํ…Œ์ŠคํŠธ | +|---|---|---|---| +| control | ์—†์Œ | PASSED | PASSED | +| M1 | `lib.sh:771` idle ํ•„ํ„ฐ๋ฅผ `if False:` ๋กœ(idle pane ์„ ์ฐพ์ง€ ๋ชปํ•จ) | **FAILED** โ€” "an idle pane must be reused, not split around"(`w2:p1` ์—์„œ split ๋ฐœ์ƒ) | PASSED | +| M2 | `lib.sh:855` rename ๋Œ€์ƒ์„ ์ฒซ ํ›„๋ณด(`${candidate_wss%%[[:space:]]*}`)๋กœ | PASSED | **FAILED** โ€” `[['workspace','rename','w1','team-b']]` โ‰  ๊ธฐ๋Œ€๊ฐ’(w2) | + +- ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ์ž๊ธฐ ์ฝ”๋“œ ๊ฒฝ๋กœ์˜ ๋ณ€์ด์—์„œ๋งŒ ์‹คํŒจํ•˜๊ณ , ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์˜ ๋ณ€์ด์—๋Š” ์˜ํ–ฅ๋ฐ›์ง€ ์•Š๋Š”๋‹ค. ๋‘ ํ…Œ์ŠคํŠธ๊ฐ€ ๋ธŒ๋ฆฌํ”„์˜ "idle pane ์šฐ์„ "๊ณผ "rename ๋Œ€์ƒ"์„ **๊ฐ๊ฐ ๋…๋ฆฝ์ ์œผ๋กœ ๊ณ ์ •**ํ•จ์ด ํ™•์ธ๋˜์—ˆ๋‹ค. +- ๋ณ€์ด ์‹คํ–‰ ์ „ํ›„ ์‹ค์ œ `.mam/jobs` ํ•ญ๋ชฉ ์ˆ˜๋Š” 2401 ๋กœ ๋™์ผํ–ˆ๋‹ค. ์‚ฌ๋ณธ์€ ๋๋‚˜๊ณ  ์‚ญ์ œํ–ˆ๋‹ค. + +## 4. ๋ฒ„์ „ ๋ฝ์Šคํ… ยท ๋ฆด๋ฆฌ์Šค ์ปค๋ฐ‹ โ€” โœ… +- `lib.sh:32` `MAM_VERSION="4.4.4"`, `eager.py:16` `FRAMEWORK_VERSION = "4.4.4"`, SKILL.md 8๊ฐœ `version: 4.4.4`. +- `verify_framework_version()` ํ†ต๊ณผ(`env -u MAM_VERSION`). +- ํ˜„์žฌ ๋ฌธ์„œ(READMEยทREADME.koยทBOOTSTRAPยทBOOTSTRAP.koยทMESSAGING) ๋ชจ๋‘ 4.4.4. +- `VERSIONS.md` ๋ฐ–์— ๋‚จ์€ `4.4.3` ์€ ๋ณด๊ด€๋œ ๋ฆฌ๋ทฐ ๋ฆฌํฌํŠธ(`.agents/reports/โ€ฆ`)์˜ ์ด๋ ฅ ์„œ์ˆ ๋ฟ์ด๋‹ค. +- ๋ฆด๋ฆฌ์Šค ์ปค๋ฐ‹ `4176d75` ์ œ๋ชฉ์€ `release(v4.4.4): bump framework and eight skills to 4.4.4 in lockstep` ๊ทธ๋Œ€๋กœ๋‹ค. +- โš ๏ธ ๋น„์ฐจ๋‹จ: `VERSIONS.md:51` "์‹ ๊ทœ ํ…Œ์ŠคํŠธ **8๊ฐœ** โ€ฆ `test_issue7_workspace_packing.py` **3๊ฐœ**" ๋Š” `ea911d7` ์ดํ›„ **10๊ฐœ / 5๊ฐœ**๋‹ค(ยง6-1). + +## 5. ํ…Œ์ŠคํŠธ ยท ์ •์  ๊ฒ€์‚ฌ (HEAD `ea911d7`) +- **๋ธŒ๋ฆฌํ”„ ์ง€์ • 10ํŒŒ์ผ + `test_version_consistency` + `test_deploy_freshness`**: **275 passed in 154.9 s, rc 0**. + - ์‹คํ–‰ ์‹œ์  HEAD `ea911d7` ์„ ๊ธฐ๋ก์œผ๋กœ ํ™•์ธํ–ˆ๋‹ค. + - ์‹คํ–‰ ์ „ํ›„ ์‹ค์ œ `.mam/jobs` ๋ ˆ์ฝ”๋“œ ๋ชฉ๋ก์ด ๋™์ผํ–ˆ๋‹ค(์ถ”๊ฐ€ 0๊ฑด). +- **์ •์  ๊ฒ€์‚ฌ**: + - `git diff --check 8a09e02...HEAD` ํด๋ฆฐ + - `bash -n deploy/update.sh` ํ†ต๊ณผ + - ์ž‘์—… ํŠธ๋ฆฌ ๊นจ๋— + - shellcheck ๋Š” ๋ฏธ์„ค์น˜๋ผ ์‹คํ–‰ํ•˜์ง€ ๋ชปํ–ˆ๋‹ค(๋Œ€์‹  ยง2 ์ƒŒ๋“œ๋ฐ•์Šค ์‹คํ–‰์œผ๋กœ ํ™•์ธ). + +## 6. ๋น„์ฐจ๋‹จ ๊ถŒ๊ณ  (ํ›„์†) +1. **`VERSIONS.md:51` ์‹ ๊ทœ ํ…Œ์ŠคํŠธ ์ˆ˜ ์ •์ •**: 8 โ†’ 10, #7 3 โ†’ 5. ์„ค๋ช…์— "๋’ค ํ›„๋ณด์˜ idle pane ์žฌ์‚ฌ์šฉ, ์„ ํƒ๋œ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋งŒ rename" ์ถ”๊ฐ€. ๋ณ‘ํ•ฉ ์ „ ํ•œ ์ค„ ์ˆ˜์ •์„ ๊ถŒํ•œ๋‹ค. +2. `scrub-secrets` ์„œ๋ธŒ์ปค๋งจ๋“œ ์š”์•ฝ ๋„์›€๋ง `"remove broker.password from every job record"` ์€ ์ด์ œ ์•ฝ๊ฐ„ ๊ณผ์žฅ์ด๋‹ค. `--dry-run` ๋„์›€๋ง์€ ์ •ํ™•ํ•˜๋‹ค. ๊ณ ์ • ์บก์ฒ˜ `help-registry.txt` ์™€ ํ•จ๊ป˜ ๊ฐฑ์‹ ํ•  ๊ฒƒ. +3. `DelegationService` ๋ฅผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์“ธ ๋•Œ `ensure_env_loaded()` ์„ ํ–‰, ๋ช…์‹œ broker override ์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ env ๋กœ ์ œ๊ณตํ•œ๋‹ค๋Š” ์ ์„ ๋ฌธ์„œํ™”. +4. ํ† ํฐ ์šฐ๋ฐœ ๋…ธ์ถœ ์‹ฌ์ธต ๋ฐฉ์–ด(์ด์ „ ํŠธ๋ฆฌ์•„์ง€ ๊ถŒ๊ณ  ์œ ์ง€). + +## 7. GM ์ฐธ๊ณ  +- ์ด ํŒ๋‹จ์€ `0cf8d1c0` ๋ฆฌํฌํŠธ์™€ ๊ฐ™์€ HEAD(`ea911d7`)์— ๋Œ€ํ•œ ๊ฒƒ์ด๊ณ , ๊ฒฐ๋ก ๋„ ๊ฐ™๋‹ค. +- `0cf8d1c0` ์—์„œ ์ง€์ ํ•œ "๋ฆฌ๋ทฐ ์ค‘ ์ปค๋ฐ‹" ๋ฌธ์ œ๋Š” ์ด๋ฒˆ ๋ธŒ๋ฆฌํ”„๊ฐ€ `ea911d7` ์„ ๋Œ€์ƒ์œผ๋กœ ๋ช…์‹œํ•˜๋ฉด์„œ ํ•ด์†Œ๋˜์—ˆ๋‹ค. ๋ณ‘ํ•ฉ ์‹œ์—๋Š” agyยทgrok ์˜ 2ํšŒ์ฐจ ๋ฆฌํฌํŠธ๋„ `ea911d7` ์„ ๋Œ€์ƒ์œผ๋กœ ํ–ˆ๋Š”์ง€ ํ™•์ธํ•  ๊ฒƒ. + +## 8. ์ข…ํ•ฉ +- N1 ์€ ์ตœ์†Œ ์ˆ˜์ •์œผ๋กœ ํ•ด์†Œ๋˜์—ˆ๋‹ค. ์ƒŒ๋“œ๋ฐ•์Šค, ์‹ค์ œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ฝ๊ธฐ ์ „์šฉ ์ง‘๊ณ„(์„ ํƒ 0๊ฑด), ํšŒ๊ท€ ํ…Œ์ŠคํŠธ๋กœ ํ™•์ธํ–ˆ๋‹ค. +- `ea911d7` ์˜ `update.sh` ๋ณ‘ํ•ฉ ๋ณด๊ฐ•์€ ์„ธ ๊ฐ€์ง€ ๊ฐ€์žฅ์ž๋ฆฌ๋ฅผ ๋ฐ”๋ฅด๊ฒŒ ๊ณ ์ณค๋‹ค. ์ƒˆ #7 ํ…Œ์ŠคํŠธ 2๊ฑด์€ ํ•ด๋‹น ์ฝ”๋“œ ๊ฒฝ๋กœ๋ฅผ ์‹ค์ œ๋กœ ๊ณ ์ •ํ•œ๋‹ค(ยง3-2). +- ๋ฒ„์ „ ๋ฝ์Šคํ…์€ ์™„์ „ํ•˜๋‹ค. ๋‚จ์€ ๊ฒƒ์€ `VERSIONS.md:51` ํ…Œ์ŠคํŠธ ์ˆ˜ ํ•œ ์ค„(๋น„์ฐจ๋‹จ)๋ฟ์ด๋‹ค. ์„ค๊ณ„ ์žฌ๊ฒ€ํ† ๋Š” ํ•„์š” ์—†๋‹ค. + +[VERDICT: PASS] diff --git a/.agents/reports/planner-reviewer-codex-01/report-737157bc.md b/.agents/reports/planner-reviewer-codex-01/report-737157bc.md new file mode 100644 index 0000000..f398d19 --- /dev/null +++ b/.agents/reports/planner-reviewer-codex-01/report-737157bc.md @@ -0,0 +1,65 @@ +# Codex review โ€” fix/issue-12-8-7 + +Job: `737157bc`. Reviewer: `planner-reviewer-codex-01`. +Base: `8a09e02`. Reviewed HEAD: `fbe27319802a8403ed21691ba59a2c3d6e5c5319`. + +PASS. No blocking defect found in the requested branch changes. Reviewed the full branch diff and six commits through fbe2731; archived peer reports were background context, not substitutes for fresh verification. + +## Issue #12 โ€” broker secrets + +`BrokerConfig.to_registry_block()` excludes the password. `_atomic_write_record()` removes the key before persistence, including caller-supplied broker blocks and subsequent writes to old records. Because the in-memory registration record is scrubbed before `init_job_log`, newly seeded metadata also receives the scrubbed block. + +`broker_config_from_job()` overlays record routing/authentication metadata on environment configuration, retaining MQTT_PASSWORD when the record omits it. Publisher CLI and job subscriber call `ensure_env_loaded()` before preparing or connecting; reconcile's embedded subscriber also loads the environment before building its configuration. The service publishes a prepared event using the same configuration resolver and transports. As required by the existing D6 library contract, a direct embedding application's entry point must initialize the environment (or export the variables); DelegationService itself does not load .mam.env. This prerequisite should not be confused with automatic dotenv loading in every library call. + +The migration holds the registry lock during selection and rewriting. Only a truthy stored password is selected. An independent synthetic temporary-registry check confirmed: + +- Dry-run leaves all record bytes unchanged. +- Missing, null, and empty-string passwords remain byte-identical after actual migration. +- The valued-password record loses only that key; status, sequence, auth token and dispatch token remain unchanged. + +The G6 exception for `_cmd_scrub_secrets` is narrowly justified: it changes stored credential data without a lifecycle transition. Retaining auth_token and dispatch_token supports the agent's per-job HMAC signing and attempt binding. This is removal of shared broker credentials from live job records and future writes, not a claim to erase historical transcripts or old audit copies. Legacy record passwords remain usable until scrubbed; environment reconstruction was independently checked with synthetic data. + +## Issue #8 โ€” update state + +Successful updates keep the installer's new version and hash ledgers. Only old install_state keys absent from the fresh file are merged. Exact awk field comparison avoids regex-key interpretation, lines without `=` and empty keys are skipped, the fresh file's final newline is repaired, and an unterminated old final line is processed. An independent execution of the actual merge block in temporary directories verified these properties with a dotted key, a similar existing key, a malformed line and both missing-final-newline cases. + +The failure restoration function is unchanged and still copies install_state, asset_hashes.txt and version.txt from backup. The passing Issue #8 test performs a real local installโ†’stale-metadataโ†’update cycle. Failure-path preservation was checked by code comparison; a separate injected-installer-failure test was not run. + +## Issue #7 โ€” workspace packing + +Candidate workspace IDs are deduplicated in pane order after normalized CWD matching. Each candidate first offers an agent-less pane; otherwise the unchanged layout engine determines a split. Overflow continues to the next candidate. Selection sets existing_ws before allocating or reusing, and rename applies only to the chosen workspace. Single-candidate behavior and the existing agent-reuse path remain intact. + +The shell loop splits machine workspace IDs, not CWDs or display labels. Under the existing herdr ID representation this is appropriate; arbitrary whitespace or glob characters in future ID formats would require a different iteration form. No new handling of labels as IDs was introduced. + +The opt-in mock geometry represents one full pane, two full-height columns, a three-pane partial grid, and a full four-pane grid per sampled workspace. Those shapes match the unchanged layout engine's grouping and overflow rules. Passing tests cover sibling packing, all-full allocation, later idle-pane reuse, chosen-workspace rename and single-workspace equivalence, alongside the broader shim and layout tests. + +## Heartbeat test and release + +fbe2731 still invokes the real publisher and checks eventual registry completion and wrapper success. Launching publishes asynchronously supplies disk heartbeats at a timer-like cadence independently of broker ACK latency, consistent with local recording before network publication. It does not change production wrapper behavior, and separate no-signal and overall-deadline tests remain. + +Nonblocking coverage limit: the test still uses broker timing and does not explicitly synchronize on subscriber idle exit. It demonstrates periodic-heartbeat behavior, not equivalence to every agent that serially waits for each publish. A deterministic subscriber-exit signal would strengthen proof that the disk-grace branch was exercised under every broker timing. The current fresh run passed. + +Version constants, all eight skill frontmatters, current documentation and the VERSIONS matrix are synchronized at 4.4.4. The release commit is exactly `release(v4.4.4): bump framework and eight skills to 4.4.4 in lockstep`. The checked current documents retain 4.4.3 only as a historical changelog heading. The changelog's original new-test count predates the two additional packing tests; that is a documentation bookkeeping nit, not a runtime failure. + +## Fresh validation + +**242 passed in 213.89 seconds** at the reviewed HEAD: + +```text +tests/test_issue7_workspace_packing.py +tests/test_issue8_update_state.py +tests/test_issue12_registry_secrets.py +tests/test_issue5_phase1_delegation.py +tests/test_issue5_phase2a_loop.py +tests/test_herdr_shim_contract.py +tests/test_layout.py +tests/test_deploy_layout.py +tests/test_issue14_delivery_ack.py +tests/test_version_consistency.py +``` + +Full-branch `git diff --check`, individual `bash -n` checks for lib.sh and update.sh, and `verify_framework_version()` using the shell constant all passed. The independent scrub, configuration and merge checks above also passed. No full repository suite was rerun. + +Review only: no source changes, commits, pushes, merges, production registry migration, or real-conversation purge. Synthetic migration identifiers were generated with uuid4; all mutation probes used temporary directories. Final HEAD and tracked-worktree checks were clean. Progress events were published during review and testing. + +[VERDICT: PASS] diff --git a/.agents/reports/reviewer-agy-01/report-3f5d1cd4.md b/.agents/reports/reviewer-agy-01/report-3f5d1cd4.md new file mode 100644 index 0000000..f7248da --- /dev/null +++ b/.agents/reports/reviewer-agy-01/report-3f5d1cd4.md @@ -0,0 +1,151 @@ +# Peer Review Report: Branch `fix/issue-12-8-7` (Round 2 โ€” Commits `0fedae2` & `ea911d7`) + +- **Reviewer**: agy (session: `herdr:reviewer-agy-01`) +- **Job ID**: `3f5d1cd4` +- **Role**: Reviewer +- **Target Branch**: `fix/issue-12-8-7` +- **Base Commit**: `8a09e02` (v4.4.3) +- **Evaluated Commits**: + - `e629cc1` โ€” `fix(issue-12,8,7): keep broker password out of records, keep fresh install stamps, pack sibling workspaces` + - `4176d75` โ€” `release(v4.4.4): bump framework and eight skills to 4.4.4 in lockstep` + - `0fedae2` โ€” `fix(issue-12): scrub-secrets selects only records with a stored password` + - `ea911d7` โ€” `fix(issue-8,7): harden install_state merge and pin sibling-workspace edge cases` + +--- + +## 1. Executive Summary + +This Round 2 peer review evaluated the complete branch `fix/issue-12-8-7` up to `ea911d7`, focusing specifically on the resolutions introduced in commits `0fedae2` (N1 fix from Claude's Round 1 review: value-based filtering in `scrub-secrets`) and `ea911d7` (non-blocking hardening recommendations for `update.sh` `install_state` merge and two new edge-case tests for Issue #7 workspace packing). + +Key verifications completed: +1. **Commit `0fedae2` (N1 Resolution)**: `_cmd_scrub_secrets` now strictly tests `broker.get("password")` (value truthiness) rather than key presence. Unauthenticated records with `"password": null` (~900 in realistic registries) are ignored, preventing spurious rewrite churn and lock contention. Dry-run verified on live `.mam/jobs` (0 records reported). +2. **Commit `ea911d7` (Update Hardening)**: `deploy/update.sh` ensures trailing newline on preexisting `install_state`, skips lines missing `=`, and utilizes `awk` with literal variable passing for exact key comparison (`$1 == k`). Failure recovery via `restore_on_failure` is intact. +3. **Commit `ea911d7` (Issue #7 Workspace Edge Cases)**: Two new automated tests in `test_issue7_workspace_packing.py` confirm: + - An idle pane in a subsequent candidate workspace is reused before attempting splits or workspace creation. + - `MAM_WS_LABEL` rename applies exclusively to the chosen target workspace. +4. **Version Lockstep v4.4.4**: Maintained consistently across `lib.sh`, `eager.py`, 8 `SKILL.md` manifests, `VERSIONS.md`, and documentation files. +5. **Test Suite Verification**: 13/13 targeted tests and 231/231 regression tests passed cleanly with 0 job registry leaks. + +--- + +## 2. Review of Commit `0fedae2` (Issue #12: Value-Based Password Scrubbing) + +### 2.1 Value-Based Record Selection +- In `.agents/skills/lib_py/delegation/registry.py` (`_cmd_scrub_secrets`): + ```python + # Old: + if not (isinstance(broker, dict) and "password" in broker): + continue + + # New (0fedae2): + if not (isinstance(broker, dict) and broker.get("password")): + continue + ``` +- **Analysis**: + - Legacy records created under previous versions without authentication stored `"password": null`. + - Testing `broker.get("password")` ensures only truthy values (non-empty strings containing actual passwords) trigger migration. + - Records with `None`, `""`, or omitted `"password"` keys are untouched. + - Verified on live workspace: `registry.py --registry-dir .mam/jobs scrub-secrets --dry-run` reports `would scrub 0 record(s) under .mam/jobs`, avoiding unnecessary rewrites. + +### 2.2 Documentation & Specification Alignment +- `registry.md`: Clarifies that `scrub-secrets` strips stored passwords, leaving empty/null keys to be dropped naturally on the record's next write via `_atomic_write_record`. +- `VERSIONS.md`: Section 2 updated to emphasize that `"password": null` is not treated as a secret and is neither reported nor rewritten. + +### 2.3 Automated Test Coverage +- In `tests/test_issue12_registry_secrets.py`, `test_scrub_secrets_cli_migrates_a_registry` was expanded to create an unauthenticated job record with `"password": None`. +- Verifies: + - `empty not in dry.stdout` (dry run ignores null password). + - `empty_path.read_text() == empty_before` (file is not modified). + - Only genuine leaked passwords are reported and scrubbed. + +--- + +## 3. Review of Commit `ea911d7` (Issue #8: `update.sh` Merge Hardening) + +### 3.1 Install State Trailing Newline Guard +- In `deploy/update.sh`: + ```bash + if [ -s .mam/install_state ] && [ -n "$(tail -c1 .mam/install_state)" ]; then + printf '\n' >> .mam/install_state + fi + ``` + Guarantees that an existing `install_state` missing a final newline does not concatenate appended keys onto the last line. + +### 3.2 Non-KV Line Rejection +- Lines lacking `=` are skipped safely: + ```bash + case "$line" in + *=*) ;; + *) continue ;; + esac + ``` + +### 3.3 Exact Key Matching without Regex Side-Effects +- Previously used `grep -q "^${key}="`, which could interpret regex metacharacters in key names. +- Now uses `awk`: + ```bash + if ! awk -F= -v k="$key" '$1 == k { found = 1 } END { exit !found }' .mam/install_state; then + printf '%s\n' "$line" >> .mam/install_state + fi + ``` +- Exact literal string equality `$1 == k` prevents false matches and escapes. +- Full cycle verified via `tests/test_issue8_update_state.py`. + +--- + +## 4. Review of Commit `ea911d7` (Issue #7: Workspace Packing Edge Cases) + +Two new regression tests were added to `tests/test_issue7_workspace_packing.py`: + +1. `test_idle_pane_in_a_later_workspace_is_reused`: + - Configures `w1` as full (4 agents) and `w2` with 1 active agent and 1 idle pane (`w2:p_idle`). + - Starts 7th agent: verifies the candidate traversal checks `w1`, continues past overflow, checks `w2`, finds the idle pane, and starts the agent directly on `w2:p_idle` without splitting or creating a workspace. +2. `test_ws_label_rename_targets_only_the_chosen_workspace`: + - Configures `w1` (4 agents, full) and `w2` (1 agent). + - Starts 8th agent with `MAM_WS_LABEL="team-b"`. + - Confirms that only the target workspace `w2` is renamed: `[["workspace", "rename", "w2", "team-b"]]`. Preceding full candidate workspaces are untouched. + +Both tests pass and accurately pin critical edge-case behaviors. + +--- + +## 5. Version Lockstep v4.4.4 Verification + +All components remain strictly locked at `4.4.4`: +- `.agents/skills/lib.sh`: `MAM_VERSION="4.4.4"` +- `.agents/skills/lib_py/loop/eager.py`: `FRAMEWORK_VERSION = "4.4.4"` +- Frontmatter `version: 4.4.4` across all 8 skills (`create`, `stop`, `resume`, `status`, `monitor`, `delegate-job`, `loop`, `orc-onboard`). +- `VERSIONS.md`: Current release v4.4.4, 8-row skills matrix, v4.4.4 changelog. +- Badges and headers in `README.md`, `README.ko.md`, `BOOTSTRAP.md`, `BOOTSTRAP.ko.md`, and `MESSAGING.md`. +- `lib_py.loop.eager.verify_framework_version()`: Verified OK. + +--- + +## 6. Test Suite & Registry Integrity Audit + +### 6.1 Targeted Test Execution +Command: +```bash +.venv/bin/python -m pytest tests/test_issue7_workspace_packing.py tests/test_issue8_update_state.py tests/test_issue12_registry_secrets.py tests/test_version_consistency.py +``` +**Result**: 13 passed in 19.47s (100% success). + +### 6.2 Regression Test Execution +Command: +```bash +.venv/bin/python -m pytest tests/test_herdr_shim_contract.py tests/test_layout.py tests/test_deploy_layout.py tests/test_issue5_phase1_delegation.py tests/test_issue5_phase2a_loop.py tests/test_loop_cli.py tests/test_issue5_phase4_runtime.py +``` +**Result**: 231 passed in 128.99s (100% success). + +### 6.3 Sandboxing & Leak Verification +- Baseline `.mam/jobs` file count before execution: **2405** +- File count after full test execution: **2405** +- Leak delta: **0 files created or modified** in live registry. + +--- + +## 7. Conclusion + +The updates in `0fedae2` and `ea911d7` cleanly resolve the N1 finding and further harden update and workspace packing operations. All verification criteria are fully satisfied. + +[VERDICT: PASS] diff --git a/.agents/reports/reviewer-grok-01/report-cc059645.md b/.agents/reports/reviewer-grok-01/report-cc059645.md new file mode 100644 index 0000000..ced2330 --- /dev/null +++ b/.agents/reports/reviewer-grok-01/report-cc059645.md @@ -0,0 +1,62 @@ +# ๐Ÿ” Review Report โ€” Job cc059645 (`fix/issue-12-8-7` 2ํšŒ์ฐจ @ `ea911d7`) + +- **Reviewer**: `reviewer-grok-01` (role: reviewer) ยท 2026-09-13 +- **๋ฒ”์œ„**: `git log`/`git diff` **`8a09e02...HEAD`** (๋ฃจํ”„ ์ˆ˜์ง‘ diff ๋Š” ๋น„์–ด ์žˆ์Œ) +- **HEAD**: `ea911d7` `fix(issue-8,7): harden install_state merge and pin sibling-workspace edge cases` +- **1ํšŒ์ฐจ HEAD**: `4176d75` (grok PASS). ์ด๋ฒˆ ์ดˆ์ : `0fedae2` (claude N1) + `ea911d7` (๋น„์ฐจ๋‹จ ๊ถŒ๊ณ ) +- **์•ˆ์ „**: ์‹ค์ œ `.mam/jobs` ์— `scrub-secrets` ๋ฏธ์‹คํ–‰. ์‹ค์ œ ํ™ˆ purge ์—†์Œ. ์†Œ์Šค ๋ฌด๋ณ€๊ฒฝ. codex ์„ธ์…˜ ๋ฏธ์ ‘์ด‰. + +์ปค๋ฐ‹: `e629cc1` โ†’ `4176d75` โ†’ **`0fedae2`** โ†’ **`ea911d7`**. `git diff --check 8a09e02...HEAD` ํด๋ฆฐ. + +## (1) `0fedae2` N1 โ€” scrub-secrets ๊ฐ’ ๊ธฐ์ค€ ์„ ํƒ + +`_cmd_scrub_secrets` (`registry.py:360`): `"password" in broker` โ†’ `broker.get("password")`. JSON `null`ยท๋นˆ ๋ฌธ์ž์—ดยทํ‚ค ์—†์Œ์€ ๋ชจ๋‘ falsy ๋ผ ๋ชฉ๋ก์—๋„ ์žฌ๊ธฐ๋ก์—๋„ ์•ˆ ๋„ฃ์Œ. ๊ฐ’์ด ์žˆ๋Š” ๋ฌธ์ž์—ด๋งŒ pop ํ›„ `_atomic_write_record`. lock ๋ฒ”์œ„๋Š” ๊ทธ๋Œ€๋กœ ์ „์ฒด ์ˆœํšŒ์ง€๋งŒ **์“ฐ๊ธฐ๋Š” ๋น„๋ฐ€์ด ์žˆ๋Š” ๋ ˆ์ฝ”๋“œ๋งŒ**. + +`test_scrub_secrets_cli_migrates_a_registry`: dirty(`"leaked-earlier"`) 1๊ฑด๋งŒ dry-run/์‹ค์ œ ๋Œ€์ƒ. `password: null` ๋ ˆ์ฝ”๋“œ๋Š” stdout ์— id ๊ฐ€ ์—†๊ณ  ํŒŒ์ผ ๋ฐ”์ดํŠธ ๋ถˆ๋ณ€. ํ‚ค ์—†๋Š” clean ๋ ˆ์ฝ”๋“œ๋„ ๋ถˆ๋ณ€. `--dry-run` ์€ dirty ํŒŒ์ผ์˜ ์œ ์ถœ ๋ฌธ์ž์—ด์„ ๊ทธ๋Œ€๋กœ ๋‘ . 4176d75 ์—์„œ๋Š” null ํ‚ค๊นŒ์ง€ ์„ธ์–ด `would scrub 2` ๊ฐ€ ๋‚ฌ์„ ํ…Œ์ŠคํŠธ. + +`_atomic_write_record` ์˜ `_scrub_record_secrets` ๋Š” ์—ฌ์ „ํžˆ ํ‚ค๋งŒ ๋ณด๋ฉด pop โ€” null ํ‚ค๋Š” **๋‹ค์Œ ์ผ๋ฐ˜ ์“ฐ๊ธฐ** ๋•Œ ์ •๋ฆฌ. scrub ๊ฐ€ 901๊ฑด์„ ์žฌ๊ธฐ๋กํ•˜์ง€ ์•Š๋Š” ๋ถ„์—…๊ณผ ๋งž์Œ. + +๋ฌธ์„œ: `VERSIONS.md` v4.4.4 ํ•ญ๋ชฉ 2, `registry.md:60-62` ๊ฐ€ โ€œ์ €์žฅ๋œ ๊ฐ’๋งŒ / null ์€ ๋‹ค์Œ ์“ฐ๊ธฐโ€๋กœ ์ •ํ•ฉ. + +## (2) `ea911d7` #8 โ€” install_state ๋ณ‘ํ•ฉ ๋ณด๊ฐ• + +`deploy/update.sh:243-256`: +- ๋น„์–ด ์žˆ์ง€ ์•Š๊ณ  ๋งˆ์ง€๋ง‰ ๋ฐ”์ดํŠธ๊ฐ€ ๊ฐœํ–‰์ด ์•„๋‹ˆ๋ฉด (`tail -c1` + `$(โ€ฆ)` ๊ฐ€ trailing newline ์„ ์ง€์šฐ๋Š” ๊ด€์šฉ) `printf '\n'` โ€” ์ฒซ ๋ณ‘ํ•ฉ ํ‚ค๊ฐ€ ๋งˆ์ง€๋ง‰ ์ค„์— ๋ถ™์ง€ ์•Š์Œ. +- `case *=*` ๋กœ `=` ์—†๋Š” ์ค„ ๋ฌด์‹œ. +- `awk -F= -v k="$key" '$1 == k'` ์ •ํ™• ๋ฌธ์ž์—ด ๋น„๊ต. `grep -q "^${key}="` ์˜ ์ •๊ทœ์‹ ํ•ด์„ ์—†์Œ. +- ์‹คํŒจ `restore_on_failure` (`:168-171`) ๋Š” ์„ธ ํŒŒ์ผ `cp -f` ๊ทธ๋Œ€๋กœ. ์„ฑ๊ณต ๊ฒฝ๋กœ๋งŒ ๋ณ€๊ฒฝ. + +`test_issue8_update_state.py` ์‹ค์ œ installโ†’stale stampsโ†’`update.sh --force` ํ†ต๊ณผ(์•„๋ž˜ ์Šค์œ„ํŠธ). + +## (3) `ea911d7` #7 ํ…Œ์ŠคํŠธ 2๊ฑด โ€” ์‹ค์ œ ๊ฒฝ๋กœ + +`test_idle_pane_in_a_later_workspace_is_reused`: w1=4(overflow) + w2=1 + `state["panes"]` ์— agent ์—†๋Š” `w2:p_idle`. mock `pane list` ๋Š” ์—์ด์ „ํŠธ pane ์— `agent` ๋ฅผ ๋„ฃ๊ณ  extra pane ์€ ์•ˆ ๋„ฃ์œผ๋ฏ€๋กœ, `lib.sh:771` `not p.get('agent')` ๊ฐ€ w2 ์—์„œ idle ์„ ๊ณ ๋ฆ„. split/create ์—†์Œ, `agent start โ€ฆ --pane w2:p_idle`. idle ์šฐ์„ ์ด ๊นจ์ง€๋ฉด w2 split ์œผ๋กœ ์‹คํŒจ. + +`test_ws_label_rename_targets_only_the_chosen_workspace`: ๊ฐ™์€ 4+1 ์—์„œ `MAM_WS_LABEL=team-b`. `lib.sh:854-856` ์€ ์„ ํƒ๋œ `existing_ws` ์—๋งŒ `workspace rename`. ํ˜ธ์ถœ ๋กœ๊ทธ๊ฐ€ ์ •ํ™•ํžˆ `["workspace", "rename", "w2", "team-b"]` โ€” w1 ์ด๋‚˜ ์ƒ์„ฑ ๊ฒฝ๋กœ `--label` ์ด ์•„๋‹˜. + +## (4) 1ํšŒ์ฐจ PASS ํšŒ๊ท€ + ๋ฝ์Šคํ… + +#12 `to_registry_block` password ์ œ์™ธ, env ๋ณต์›, G6 `_cmd_scrub_secrets` ์˜ˆ์™ธ, `auth_token` ์œ ์ง€. #7 ํ›„๋ณด ์ˆœํšŒยทoverflow `continue`ยท๋‹จ์ผ ws ๋™์น˜. #8 ์„ฑ๊ณต ๊ฒฝ๋กœ version/hashes ๋น„๋ณต์›. ํšŒ๊ท€ ์—†์Œ. + +`lib.sh`/`eager.py`/8 SKILL.md/VERSIONS.md ๊ฐœ์š”ยท๋งคํŠธ๋ฆญ์Šค / READMEยทBOOTSTRAPยทMESSAGING = **4.4.4**. ๋ฆด๋ฆฌ์Šค ์ปค๋ฐ‹ `4176d75` ์ œ๋ชฉ ์œ ์ง€. `env -u MAM_VERSION` ์•„๋ž˜ `verify_framework_version` ํ†ต๊ณผ. ํ˜„์žฌ ๋ฒ„์ „ ์•ˆ๋‚ด ๋ฌธ์„œ์— ์ž”์—ฌ `4.4.3` ์—†์Œ. + +Non-blocking (1ํšŒ์ฐจ์™€ ๋™์ผ): ๋ฝ์Šคํ…์ด v4.4.3 ํ’ˆ์งˆ ๋ฌธ์žฅ์„ 24/17 โ†’ 26/19 ๋กœ ๊ณ ์นœ ๊ฒƒ์€ `fae70c1` ์ž‘์—…์ด์ง€ ์ด ๋ฒ ์ด์Šค๊ฐ€ ์•„๋‹˜. v4.4.4 ํ’ˆ์งˆ ๋ถˆ๋ฆฟ์€ ์•„์ง โ€œํ…Œ์ŠคํŠธ 8๊ฐœ/issue7 3๊ฐœโ€ โ€” ์ดํ›„ ํ…Œ์ŠคํŠธ 2๊ฑดยทN1 ๋ณด๊ฐ•์€ changelog ํ•ญ๋ชฉ 2ยท์ฝ”๋“œ์—๋งŒ ๋ฐ˜์˜. ๊ธฐ๋Šฅ ๊ฒฐํ•จ ์•„๋‹˜. + +## (5) ํ…Œ์ŠคํŠธ + +``` +env -u MAM_VERSION .venv/bin/python -m pytest -q \ + tests/test_issue7_workspace_packing.py tests/test_issue8_update_state.py \ + tests/test_issue12_registry_secrets.py tests/test_issue5_phase1_delegation.py \ + tests/test_issue5_phase2a_loop.py tests/test_herdr_shim_contract.py \ + tests/test_layout.py tests/test_deploy_layout.py tests/test_loop_cli.py \ + tests/test_issue5_phase4_runtime.py tests/test_version_consistency.py +``` + +**244 passed in 134.54s** (1ํšŒ์ฐจ 242 + #7 ํ…Œ์ŠคํŠธ 2). `bash -n` lib.sh/`update.sh` ํด๋ฆฐ. ์ „์ฒด ์Šค์œ„ํŠธ ๋ฏธ์‹คํ–‰. + +## ์ข…ํ•ฉ + +N1(null password 901๊ฑด ์žฌ๊ธฐ๋ก)์€ ๊ฐ’ ๊ธฐ์ค€์œผ๋กœ ๋‹ซํ˜”๊ณ , install_state ๋ณ‘ํ•ฉ๊ณผ #7 idle/rename ๊ฒฝ๋กœ๊ฐ€ ํ…Œ์ŠคํŠธ๋กœ ๊ณ ์ •๋๋‹ค. 1ํšŒ์ฐจ PASS ํ•ญ๋ชฉ ํšŒ๊ท€ ์—†์Œ. ์„ค๊ณ„ ์žฌ์ž‘์—… ๋ถˆํ•„์š”. + +[VERDICT: PASS] diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 9815408..fb8d439 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -29,7 +29,7 @@ export WORKSPACE_ROOT # Framework semantic version. Single runtime source of truth; kept in lockstep # with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py. # NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable. -MAM_VERSION="4.4.3" +MAM_VERSION="4.4.4" export MAM_VERSION AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}" @@ -727,33 +727,41 @@ except Exception: print(cmd) " "$final_cmd" "$kind" 2>/dev/null || echo "$final_cmd") - # W1: Resolve existing_ws by querying pane list for matching CWD (since WorkspaceInfo has no cwd key) - existing_ws=$(_real_herdr pane list 2>/dev/null | TARGET_CWD="${ws:-.}" python3 -c " + # W1: Resolve candidate workspaces by querying pane list for matching CWD + # (WorkspaceInfo has no cwd key). ISSUE-7: every workspace on this cwd is + # a candidate, in pane order; the first one with an idle pane or with + # room in its 2xK grid takes the agent, and a fresh workspace is created + # only when all of them are full. Taking just the first match found the + # full workspace every time and re-judged it overflow, so each agent past + # the fourth opened yet another workspace (4 + 1 + 1 instead of 4 + 2). + candidate_wss=$(_real_herdr pane list 2>/dev/null | TARGET_CWD="${ws:-.}" python3 -c " import sys, json, os target_ws = os.environ.get('TARGET_CWD', '') try: target_abs = os.path.realpath(target_ws) if target_ws else '' d = json.loads(sys.stdin.read()) panes = d.get('result', {}).get('panes', []) - matched_ws = '' + seen = [] for p in panes: p_cwd = os.path.realpath(p.get('cwd', '')) if p.get('cwd') else '' - if target_abs and p_cwd == target_abs and p.get('workspace_id'): - matched_ws = p.get('workspace_id') - break - print(matched_ws) + wid = p.get('workspace_id') + if target_abs and p_cwd == target_abs and wid and wid not in seen: + seen.append(wid) + print('\n'.join(seen)) except Exception: pass " 2>/dev/null || echo "") ws_id="" target_pane="" - if [ -n "$existing_ws" ]; then + existing_ws="" + for cand_ws in $candidate_wss; do + [ -n "$cand_ws" ] || continue # BUG-4: prefer an already-idle (agent-less) pane in the existing # workspace over splitting a new one โ€” avoids unbounded pane # proliferation and 2x2 grid-layout churn when a prior agent's pane # was left behind idle. - idle_pane=$(_real_herdr pane list 2>/dev/null | TARGET_WS="$existing_ws" python3 -c " + idle_pane=$(_real_herdr pane list 2>/dev/null | TARGET_WS="$cand_ws" python3 -c " import sys, json, os target_ws = os.environ.get('TARGET_WS', '') try: @@ -768,15 +776,18 @@ except Exception: " 2>/dev/null || echo "") if [ -n "$idle_pane" ]; then + existing_ws="$cand_ws" target_pane="$idle_pane" if [ -n "$env_flags" ] && [ -n "$final_cmd" ]; then idle_env="${env_flags//--env /}" idle_env="${idle_env:+$idle_env }HERDR_WORKSPACE_ID=$existing_ws" final_cmd="env $idle_env $final_cmd" fi - else + break + fi + # W2a: Determine split direction policy via pane layout - sample_pane=$(_real_herdr pane list 2>/dev/null | TARGET_WS="$existing_ws" python3 -c " + sample_pane=$(_real_herdr pane list 2>/dev/null | TARGET_WS="$cand_ws" python3 -c " import sys, json, os target_ws = os.environ.get('TARGET_WS', '') try: @@ -798,9 +809,15 @@ except Exception: sample_pane="${split_target:-$sample_pane}" fi - if [ "$split_dir" = "right" ] || [ "$split_dir" = "down" ]; then - split_json=$(_real_herdr pane split --pane "$sample_pane" --direction "$split_dir" --cwd "${ws:-.}" $env_flags --env HERDR_WORKSPACE_ID="$existing_ws" --no-focus 2>/dev/null || echo "") - target_pane=$(echo "$split_json" | python3 -c " + if [ "$split_dir" = "overflow" ]; then + # W2b: this workspace's grid is full โ€” try the next one on the same cwd + # (ISSUE-7); a fresh workspace is created below only if none has room. + continue + fi + existing_ws="$cand_ws" + [ "$split_dir" = "down" ] || split_dir="right" + split_json=$(_real_herdr pane split --pane "$sample_pane" --direction "$split_dir" --cwd "${ws:-.}" $env_flags --env HERDR_WORKSPACE_ID="$existing_ws" --no-focus 2>/dev/null || echo "") + target_pane=$(echo "$split_json" | python3 -c " import sys, json try: d = json.loads(sys.stdin.read()) @@ -809,23 +826,8 @@ try: except Exception: pass " 2>/dev/null || echo "") - elif [ "$split_dir" = "overflow" ]; then - # W2b: Overflow threshold reached โ€” force create fresh workspace - existing_ws="" - else - split_json=$(_real_herdr pane split --pane "$sample_pane" --direction right --cwd "${ws:-.}" $env_flags --env HERDR_WORKSPACE_ID="$existing_ws" --no-focus 2>/dev/null || echo "") - target_pane=$(echo "$split_json" | python3 -c " -import sys, json -try: - d = json.loads(sys.stdin.read()) - res = d.get('result', {}) - print(res.get('pane', {}).get('pane_id') or res.get('pane_id', '')) -except Exception: - pass -" 2>/dev/null || echo "") - fi - fi - fi + break + done if [ -z "$existing_ws" ] || [ -z "$target_pane" ]; then ws_json=$(_real_herdr workspace create --cwd "${ws:-.}" ${MAM_WS_LABEL:+--label "$MAM_WS_LABEL"} $env_flags --no-focus 2>/dev/null || echo "") diff --git a/.agents/skills/lib_py/delegation/mqtt_common.py b/.agents/skills/lib_py/delegation/mqtt_common.py index 533a1d5..a413779 100644 --- a/.agents/skills/lib_py/delegation/mqtt_common.py +++ b/.agents/skills/lib_py/delegation/mqtt_common.py @@ -227,13 +227,19 @@ class BrokerConfig: return asdict(self) def to_registry_block(self) -> Dict[str, Any]: - """The subset that gets persisted into a job record's broker block.""" + """The subset that gets persisted into a job record's broker block. + + Never the password (Issue #12): every sub-agent can read the job + record, so a broker secret written there ends up in agent transcripts. + ``broker_config_from_job`` layers the block over the environment, so + the password is taken from ``MQTT_PASSWORD`` (``.mam.env``) at connect + time instead. + """ return { "host": self.host, "port": self.port, "tls": self.tls, "username": self.username, - "password": self.password, } @@ -393,12 +399,23 @@ def load_job(job_id: str, registry_dir: str = DEFAULT_REGISTRY_DIR) -> Dict[str, return json.load(fh) +def _scrub_record_secrets(record: Dict[str, Any]) -> None: + """Issue #12: a broker password never belongs in a job record (see + ``BrokerConfig.to_registry_block``). Records written before that rule are + cleaned the next time anything writes them; ``registry.py scrub-secrets`` + does it for a whole registry at once.""" + broker = record.get("broker") + if isinstance(broker, dict): + broker.pop("password", None) + + def _atomic_write_record(job_id: str, registry_dir: str, record: Dict[str, Any]) -> None: """Write a record atomically: temp file in the same dir + os.replace. The rename is atomic on POSIX, so readers never observe a half-written file. Callers MUST already hold ``registry_lock`` for read-modify-write correctness.""" + _scrub_record_secrets(record) Path(registry_dir).mkdir(parents=True, exist_ok=True) path = _job_path(job_id, registry_dir) fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=f".{job_id}.", suffix=".tmp") diff --git a/.agents/skills/lib_py/delegation/registry.py b/.agents/skills/lib_py/delegation/registry.py index d73bd15..189ecb8 100755 --- a/.agents/skills/lib_py/delegation/registry.py +++ b/.agents/skills/lib_py/delegation/registry.py @@ -333,9 +333,41 @@ def _build_parser() -> argparse.ArgumentParser: p_logs.add_argument("--json", action="store_true", help="emit raw JSON lines / records instead of a table") + p_scrub = sub.add_parser( + "scrub-secrets", + help="remove broker.password from every job record (Issue #12 migration)", + ) + p_scrub.add_argument("--dry-run", action="store_true", + help="only list the records that still carry a password") + return parser +def _cmd_scrub_secrets(args) -> int: + """Issue #12: strip broker.password from records written before + to_registry_block stopped persisting it. Every other field is kept. + + Only a stored value counts. The old rule wrote `"password": null` even + for an unauthenticated broker; that empty key is no secret, and + _atomic_write_record drops it on the record's next write anyway, so + reporting or rewriting it here would list phantom secrets and hold the + registry lock over every record in a PoC registry.""" + rd = args.registry_dir + found = [] + with registry_lock(rd): + for record in list(_iter_records(rd)): + broker = record.get("broker") + if not (isinstance(broker, dict) and broker.get("password")): + continue + found.append(record.get("job_id", "?")) + if not args.dry_run: + broker.pop("password", None) + _atomic_write_record(record["job_id"], rd, record) + verb = "would scrub" if args.dry_run else "scrubbed" + print(f"{verb} {len(found)} record(s) under {rd}" + (": " + ", ".join(found) if found else "")) + return 0 + + def _operator_write(job_id: str, registry_dir: str, **fields) -> int: """Apply an operator-authority write and report a refusal on the exit status. @@ -506,6 +538,9 @@ def main(argv: Optional[List[str]] = None) -> int: if args.command == "logs": return _cmd_logs(args) + if args.command == "scrub-secrets": + return _cmd_scrub_secrets(args) + return 1 diff --git a/.agents/skills/lib_py/loop/eager.py b/.agents/skills/lib_py/loop/eager.py index 4ebe3d7..8ffa4bb 100644 --- a/.agents/skills/lib_py/loop/eager.py +++ b/.agents/skills/lib_py/loop/eager.py @@ -13,7 +13,7 @@ import sys from typing import List, Sequence # Framework version locked with framework constants -FRAMEWORK_VERSION = "4.4.3" +FRAMEWORK_VERSION = "4.4.4" CORE_MODULES: Sequence[str] = ( # Standard library modules diff --git a/.agents/skills/multi-agent-mux-create/SKILL.md b/.agents/skills/multi-agent-mux-create/SKILL.md index cc6ca7d..5af6232 100644 --- a/.agents/skills/multi-agent-mux-create/SKILL.md +++ b/.agents/skills/multi-agent-mux-create/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-create description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session โ€” never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace." -version: 4.4.3 +version: 4.4.4 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-delegate-job/SKILL.md b/.agents/skills/multi-agent-mux-delegate-job/SKILL.md index 079ea22..048bb57 100644 --- a/.agents/skills/multi-agent-mux-delegate-job/SKILL.md +++ b/.agents/skills/multi-agent-mux-delegate-job/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-delegate-job description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer." -version: 4.4.3 +version: 4.4.4 author: godopu license: MIT platforms: [linux, macos, windows] diff --git a/.agents/skills/multi-agent-mux-delegate-job/registry.md b/.agents/skills/multi-agent-mux-delegate-job/registry.md index cd902df..907cb04 100644 --- a/.agents/skills/multi-agent-mux-delegate-job/registry.md +++ b/.agents/skills/multi-agent-mux-delegate-job/registry.md @@ -42,8 +42,7 @@ Reference implementation: [`./scripts/registry.py`](./scripts/registry.py) "host": "broker.hivemq.com", "port": 1883, "tls": false, - "username": null, - "password": null + "username": null }, "topic_prefix": "python/mqtt/jobs/abc12345", "timeout_sec": 3600, @@ -55,7 +54,14 @@ Reference implementation: [`./scripts/registry.py`](./scripts/registry.py) ``` - `broker` lets `publish_event.py` connect from the record alone (env still - overrides toggles like `MQTT_TLS`). + overrides toggles like `MQTT_TLS`). It never carries the password (Issue #12): + every sub-agent can read the record, so `MQTT_PASSWORD` is layered back from + the environment (`.mam.env`) at connect time. Records written before that rule + are cleaned on their next write; `registry.py scrub-secrets [--dry-run]` strips + every stored password from a whole registry at once (an empty `null` key left + by the old rule is not a secret; it goes on the record's next write). `auth_token` and `dispatch_token` stay in the record + because the agent's own `publish_event` needs them to sign and to pin its + attempt. - `topic_prefix` โ†’ the events topic is `/events`. - `last_seq` backs the monotonic `seq` counter so it survives process restarts. - `expected_artifacts` is the hook a user `validate.sh` checks (existence/content). diff --git a/.agents/skills/multi-agent-mux-loop/SKILL.md b/.agents/skills/multi-agent-mux-loop/SKILL.md index 8d57b6b..2c07186 100644 --- a/.agents/skills/multi-agent-mux-loop/SKILL.md +++ b/.agents/skills/multi-agent-mux-loop/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-loop description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached." -version: 4.4.3 +version: 4.4.4 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-monitor/SKILL.md b/.agents/skills/multi-agent-mux-monitor/SKILL.md index 2f946cf..7c39a27 100644 --- a/.agents/skills/multi-agent-mux-monitor/SKILL.md +++ b/.agents/skills/multi-agent-mux-monitor/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-monitor description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted." -version: 4.4.3 +version: 4.4.4 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md b/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md index 5cf72d0..a3cc1e4 100644 --- a/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md +++ b/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-orc-onboard description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture." -version: 4.4.3 +version: 4.4.4 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-resume/SKILL.md b/.agents/skills/multi-agent-mux-resume/SKILL.md index 5237b5e..b18d204 100644 --- a/.agents/skills/multi-agent-mux-resume/SKILL.md +++ b/.agents/skills/multi-agent-mux-resume/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-resume description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r ` or `agy --conversation ` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk." -version: 4.4.3 +version: 4.4.4 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-status/SKILL.md b/.agents/skills/multi-agent-mux-status/SKILL.md index c713ee1..3b13993 100644 --- a/.agents/skills/multi-agent-mux-status/SKILL.md +++ b/.agents/skills/multi-agent-mux-status/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-status description: "Read-only instant snapshot of all agent herdr sessions โ€” name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop." -version: 4.4.3 +version: 4.4.4 author: godopu license: MIT platforms: [linux, macos] diff --git a/.agents/skills/multi-agent-mux-stop/SKILL.md b/.agents/skills/multi-agent-mux-stop/SKILL.md index 1dbd462..9a4f0fc 100644 --- a/.agents/skills/multi-agent-mux-stop/SKILL.md +++ b/.agents/skills/multi-agent-mux-stop/SKILL.md @@ -1,7 +1,7 @@ --- name: multi-agent-mux-stop description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) โ€” those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start." -version: 4.4.3 +version: 4.4.4 author: godopu license: MIT platforms: [linux, macos] diff --git a/BOOTSTRAP.ko.md b/BOOTSTRAP.ko.md index 1434d63..f5c26bf 100644 --- a/BOOTSTRAP.ko.md +++ b/BOOTSTRAP.ko.md @@ -2,7 +2,7 @@ ๋ณธ ๋ฌธ์„œ๋Š” `herdr_agent_orchestration` ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ๋ฐ ๋ฉ”์‹œ์ง• ๋ฐฑํ”Œ๋ ˆ์ธ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ์— ๋„์ž…ํ•˜์—ฌ ์ด์‹ํ•˜๊ณ , ์ƒˆ๋กœ์šด ๊ฐœ๋ฐœ์ž/์—์ด์ „ํŠธ๊ฐ€ ์ดˆ๊ธฐ ๊ฐ€๋™์„ ์‹œ์ž‘ํ•  ๋•Œ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š” ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ๊ตฌ์ถ• ์ ˆ์ฐจ๋ฅผ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค. -[![Version: v4.4.3](https://img.shields.io/badge/version-v4.4.3-blue.svg)](VERSIONS.md) +[![Version: v4.4.4](https://img.shields.io/badge/version-v4.4.4-blue.svg)](VERSIONS.md) ์ƒˆ๋กœ์šด ์—์ด์ „ํŠธ๋Š” ์ด ์•ˆ๋‚ด์„œ์— ๊ธฐ์ˆ ๋œ ์ ˆ์ฐจ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•˜์—ฌ ์ดˆ๊ธฐ ํ™˜๊ฒฝ์„ ์•ˆ์ •์ ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 7495c57..76cb5fe 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -2,7 +2,7 @@ This document guides you through the setup and initialization procedures required to adopt the `herdr_agent_orchestration` orchestration and messaging backplane workflow in a new project, enabling a new developer or agent to get up and running quickly. -[![Version: v4.4.3](https://img.shields.io/badge/version-v4.4.3-blue.svg)](VERSIONS.md) +[![Version: v4.4.4](https://img.shields.io/badge/version-v4.4.4-blue.svg)](VERSIONS.md) A new agent can follow the steps in this guide sequentially to establish a stable and reliable initial environment. diff --git a/MESSAGING.md b/MESSAGING.md index 02fe79f..5abf578 100644 --- a/MESSAGING.md +++ b/MESSAGING.md @@ -1,6 +1,6 @@ # Messaging System Technical Analysis & Architecture Report -Applies to: MAM v4.4.3 +Applies to: MAM v4.4.4 This report provides a comprehensive, deep-dive analysis of the messaging system implemented in the `multi-agent-mux-delegate-job` skill. It covers the MQTT broker architecture, event protocols, job lifecycles, codebase internals, cross-system integration, and a list of known limitations along with production recommendations. diff --git a/README.ko.md b/README.ko.md index 53c7177..17d11c0 100644 --- a/README.ko.md +++ b/README.ko.md @@ -2,7 +2,7 @@ Herdr์™€ MQTT ๋ธŒ๋กœ์ปค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์ถ•๋œ ๊ณ ์‹ ๋ขฐ์„ฑ **๋‹ค์ค‘ ์—์ด์ „ํŠธ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ๋ฐ ๋ฉ”์‹œ์ง• ๋ฐฑํ”Œ๋ ˆ์ธ** ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` ๋“ฑ ๋‹ค์ค‘ LLM ๋ฐฑ์—”๋“œ ์—์ด์ „ํŠธ ์ „๋ฐ˜์— ๊ฑธ์ณ ์žฅ์‹œ๊ฐ„ ์ˆ˜ํ–‰๋˜๋Š” ์ž‘์—…(์ฝ”๋“œ ์ƒ์„ฑ, ๋ฆฌํŒฉํ† ๋ง, ๋ณด์•ˆ ๊ฒ€ํ†  ๋“ฑ)์„ ์กฐ์ •, ๊ฒฉ๋ฆฌ ๋ฐ ๊ฐ์‚ฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. -[![Version: v4.4.3](https://img.shields.io/badge/version-v4.4.3-blue.svg)](VERSIONS.md) +[![Version: v4.4.4](https://img.shields.io/badge/version-v4.4.4-blue.svg)](VERSIONS.md) --- diff --git a/README.md b/README.md index 76dcb32..14dd2ee 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`). -[![Version: v4.4.3](https://img.shields.io/badge/version-v4.4.3-blue.svg)](VERSIONS.md) +[![Version: v4.4.4](https://img.shields.io/badge/version-v4.4.4-blue.svg)](VERSIONS.md) --- diff --git a/VERSIONS.md b/VERSIONS.md index 898eca4..18e1a6e 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -6,7 +6,7 @@ ## ๐Ÿ“Œ ํ˜„์žฌ ๋ฒ„์ „ ๊ฐœ์š” (Current Release) -- **ํ”„๋ ˆ์ž„์›Œํฌ ๋ฒ„์ „**: `v4.4.3` +- **ํ”„๋ ˆ์ž„์›Œํฌ ๋ฒ„์ „**: `v4.4.4` - **์ตœ์‹  ๋ฆด๋ฆฌ์Šค ์ผ์‹œ**: 2026-09-13 (KST) - **๊ธฐ์ค€ ๋ธŒ๋žœ์น˜**: `main` - **ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜**: @@ -20,23 +20,38 @@ ## ๐Ÿงญ ์Šคํ‚ฌ ํŒจํ‚ค์ง€ ๋ฒ„์ „ ๋งคํŠธ๋ฆญ์Šค (Skills Version Matrix) -๋ชจ๋“  8๊ฐœ ์Šคํ‚ฌ์€ YAML frontmatter ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(`author`, `version`, `platforms`, `environments`) ํ‘œ์ค€ํ™”๋ฅผ ํ†ตํ•ด `v4.4.3`๋กœ ๋™๊ธฐํ™”๋˜์–ด ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. +๋ชจ๋“  8๊ฐœ ์Šคํ‚ฌ์€ YAML frontmatter ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(`author`, `version`, `platforms`, `environments`) ํ‘œ์ค€ํ™”๋ฅผ ํ†ตํ•ด `v4.4.4`๋กœ ๋™๊ธฐํ™”๋˜์–ด ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. | ์Šคํ‚ฌ๋ช… | ๋ฒ„์ „ | ์—ญํ•  ๋ฐ ์ฃผ์š” ์ฑ…์ž„ | ์ƒํƒœ | | :--- | :---: | :--- | :---: | -| **`multi-agent-mux-create`** | `4.4.3` | ์—์ด์ „ํŠธ ์„ธ์…˜ ์‹ ๊ทœ ์ƒ์„ฑ ๋ฐ Herdr ์ปจํ…Œ์ด๋„ˆ ๊ฒฉ๋ฆฌ ์Šคํฐ | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-stop`** | `4.4.3` | ๋Œ€ํ™” UUID ์›์ž์  ์บก์ฒ˜ ๋ฐ ์„ธ์…˜ ์•ˆ์ „ ์ข…๋ฃŒ (Graceful Stop) | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-resume`** | `4.4.3` | ์˜จ๋””์Šคํฌ ๋Œ€ํ™” ์ปจํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ Tier-1 ์ดˆ๊ณ ์† ์„ธ์…˜ ๋ณต์› | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-status`** | `4.4.3` | ์‹ค์‹œ๊ฐ„ Herdr ์„ธ์…˜ ๋ฐ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“œ๋ฆฌํ”„ํŠธ ์Šค๋ƒ…์ƒท ์กฐํšŒ | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-monitor`** | `4.4.3` | YAML โ†” ๋Ÿฐํƒ€์ž„ ์ƒํƒœ ๊ฐ„ ์ž์œจ ์กฐ์ •์ž (Reconciler Loop) | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-delegate-job`** | `4.4.3` | MQTT ์ด๋ฒคํŠธ ์ฑ„๋„ ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ๋‹จ์œ„ ์ž‘์—… ์œ„์ž„ | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-loop`** | `4.4.3` | Planner-Creator-Reviewer 3์ž ์ž์œจ ๊ณ„ํšยท์‹คํ–‰ยทํ”ผ์–ด๋ฆฌ๋ทฐ ๋ฃจํ”„ | โœ… ๋ฐฐํฌ | -| **`multi-agent-mux-orc-onboard`** | `4.4.3` | ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ UUID ๊ฒฉ๋ฆฌ ๋“ฑ๋ก ๋ฐ ์„œ๋ธŒ ์„ธ์…˜ ์˜ค์—ผ ๋ฐฉ์ง€ | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-create`** | `4.4.4` | ์—์ด์ „ํŠธ ์„ธ์…˜ ์‹ ๊ทœ ์ƒ์„ฑ ๋ฐ Herdr ์ปจํ…Œ์ด๋„ˆ ๊ฒฉ๋ฆฌ ์Šคํฐ | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-stop`** | `4.4.4` | ๋Œ€ํ™” UUID ์›์ž์  ์บก์ฒ˜ ๋ฐ ์„ธ์…˜ ์•ˆ์ „ ์ข…๋ฃŒ (Graceful Stop) | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-resume`** | `4.4.4` | ์˜จ๋””์Šคํฌ ๋Œ€ํ™” ์ปจํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ Tier-1 ์ดˆ๊ณ ์† ์„ธ์…˜ ๋ณต์› | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-status`** | `4.4.4` | ์‹ค์‹œ๊ฐ„ Herdr ์„ธ์…˜ ๋ฐ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“œ๋ฆฌํ”„ํŠธ ์Šค๋ƒ…์ƒท ์กฐํšŒ | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-monitor`** | `4.4.4` | YAML โ†” ๋Ÿฐํƒ€์ž„ ์ƒํƒœ ๊ฐ„ ์ž์œจ ์กฐ์ •์ž (Reconciler Loop) | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-delegate-job`** | `4.4.4` | MQTT ์ด๋ฒคํŠธ ์ฑ„๋„ ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ๋‹จ์œ„ ์ž‘์—… ์œ„์ž„ | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-loop`** | `4.4.4` | Planner-Creator-Reviewer 3์ž ์ž์œจ ๊ณ„ํšยท์‹คํ–‰ยทํ”ผ์–ด๋ฆฌ๋ทฐ ๋ฃจํ”„ | โœ… ๋ฐฐํฌ | +| **`multi-agent-mux-orc-onboard`** | `4.4.4` | ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ UUID ๊ฒฉ๋ฆฌ ๋“ฑ๋ก ๋ฐ ์„œ๋ธŒ ์„ธ์…˜ ์˜ค์—ผ ๋ฐฉ์ง€ | โœ… ๋ฐฐํฌ | --- ## ๐Ÿ“‹ ๋ฒ„์ „๋ณ„ ์ƒ์„ธ ๋ณ€๊ฒฝ ๋‚ด์—ญ (Changelog) +### ๐Ÿš€ `v4.4.4` โ€” Issue #12 ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ œ๊ฑฐ, Issue #8 ์—…๋ฐ์ดํŠธ ์Šคํƒฌํ”„ ๋ณด์กด, Issue #7 ํ˜•์ œ ์›Œํฌ์ŠคํŽ˜์ด์Šค ํŒจํ‚น (2026-09-13) + +> **ํŒจ์น˜ ๋งˆ์ผ์Šคํ†ค (PATCH Release)**: ๋ชจ๋“  ์„œ๋ธŒ์—์ด์ „ํŠธ๊ฐ€ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ์žก ๋ ˆ์ฝ”๋“œ์— MQTT ๋ธŒ๋กœ์ปค ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‰๋ฌธ์œผ๋กœ ๋ณต์‚ฌ๋˜๋˜ ๋ฌธ์ œ(Issue #12), `deploy/update.sh` ๊ฐ€ ์ธ์Šคํ†จ๋Ÿฌ๊ฐ€ ์ƒˆ๋กœ ์“ด `version.txt`ยท`asset_hashes.txt` ๋ฅผ ์—…๋ฐ์ดํŠธ ์ „ ๊ฐ’์œผ๋กœ ๋˜๋Œ๋ฆฌ๋˜ ๋ฌธ์ œ(Issue #8), ๊ฐ™์€ cwd ์˜ ์ฒซ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋งŒ ๊ฒ€์‚ฌํ•ด ๋‹ค์„ฏ ๋ฒˆ์งธ ์—์ด์ „ํŠธ๋ถ€ํ„ฐ ์—์ด์ „ํŠธ๋งˆ๋‹ค ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ฅผ ์ƒˆ๋กœ ์—ด๋˜ ๋ฌธ์ œ(Issue #7)๋ฅผ ํ•ด๊ฒฐํ–ˆ๋‹ค. ์ƒˆ `registry.py scrub-secrets` ๋Š” ์ถ”๊ฐ€ ์„œ๋ธŒ์ปค๋งจ๋“œ์ด๋ฉฐ ๊ณต๊ฐœ ๊ณ„์•ฝยท์ข…๋ฃŒ ์ฝ”๋“œ ๋ณ€๊ฒฝ์€ ์—†๋‹ค. ๋ธŒ๋กœ์ปค ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ด์ œ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ํ™˜๊ฒฝ(`MQTT_PASSWORD`, `.mam.env`)์—์„œ๋งŒ ์˜จ๋‹ค. + +#### ๐ŸŒŸ ์ฃผ์š” ๊ฐœ์„  ์‚ฌํ•ญ +1. **์žก ๋ ˆ์ฝ”๋“œ์˜ ๋ธŒ๋กœ์ปค ๋น„๋ฐ€๋ฒˆํ˜ธ ์ œ๊ฑฐ (Issue #12, `mqtt_common.BrokerConfig.to_registry_block`, `registry.py`)**: `to_registry_block` ์ด ๋” ์ด์ƒ `password` ๋ฅผ ๋ ˆ์ฝ”๋“œ์˜ `broker` ๋ธ”๋ก์— ๋„ฃ์ง€ ์•Š๋Š”๋‹ค. `broker_config_from_job` ์€ ๋ ˆ์ฝ”๋“œ ๋ธ”๋ก์„ ํ™˜๊ฒฝ ์„ค์ • ์œ„์— ๊ฒน์น˜๋ฏ€๋กœ ์ ‘์† ์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” `MQTT_PASSWORD`(`.mam.env`)์—์„œ ์ฑ„์›Œ์ง€๊ณ , `publish_event` ๋Š” ์—ฌ์ „ํžˆ ๋ ˆ์ฝ”๋“œ๋งŒ์œผ๋กœ ์ ‘์†ํ•œ๋‹ค. `_atomic_write_record` ๋Š” ์“ฐ๋Š” ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ์—์„œ `broker.password` ๋งŒ ์ง€์šฐ๋ฏ€๋กœ(๋‹ค๋ฅธ ํ•„๋“œ๋Š” ๊ทธ๋Œ€๋กœ) ์ด์ „ ๊ทœ์น™์œผ๋กœ ์“ฐ์ธ ๋ ˆ์ฝ”๋“œ๋Š” ๋‹ค์Œ ์“ฐ๊ธฐ ๋•Œ ์ •๋ฆฌ๋œ๋‹ค. `auth_token`ยท`dispatch_token` ์€ ์—์ด์ „ํŠธ ์ž์‹ ์˜ `publish_event` ๊ฐ€ ์„œ๋ช…๊ณผ attempt ๊ณ ์ •์— ์“ฐ๋ฏ€๋กœ ๋ ˆ์ฝ”๋“œ์— ๋‚จ๊ธด๋‹ค. +2. **`registry.py scrub-secrets [--dry-run]` ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (Issue #12)**: ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ lock ์•„๋ž˜์—์„œ **๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ’์ด ์‹ค์ œ๋กœ ์ €์žฅ๋œ** ๋ ˆ์ฝ”๋“œ๋งŒ ๊ณจ๋ผ ์ง€์šฐ๊ณ  ์žฌ๊ธฐ๋กํ•œ๋‹ค. ์ด์ „ ๊ทœ์น™์ด ๋ฌด์ธ์ฆ ๋ชจ๋“œ์—์„œ๋„ ๋‚จ๊ธด `"password": null` ํ‚ค๋Š” ๋น„๋ฐ€์ด ์•„๋‹ˆ๋ฏ€๋กœ ๋Œ€์ƒ๋„ ๋ณด๊ณ ๋„ ์•„๋‹ˆ๋ฉฐ, ๊ทธ ๋ ˆ์ฝ”๋“œ์˜ ๋‹ค์Œ ์“ฐ๊ธฐ ๋•Œ `_atomic_write_record` ๊ฐ€ ์ •๋ฆฌํ•œ๋‹ค. `--dry-run` ์€ ๋Œ€์ƒ ์žก ๋ชฉ๋ก๋งŒ ์ถœ๋ ฅํ•˜๊ณ  ์•„๋ฌด๊ฒƒ๋„ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. `registry.md` ๊ฐ€ ๋ธŒ๋กœ์ปค ๋ธ”๋ก ๊ทœ์น™(๋น„๋ฐ€๋ฒˆํ˜ธ ๋น„์ €์žฅ, ํ™˜๊ฒฝ์—์„œ ๋ณต์›, ํ† ํฐ ์œ ์ง€ ์ด์œ )์„ ๋ฌธ์„œํ™”ํ•œ๋‹ค. +3. **์—…๋ฐ์ดํŠธ ํ›„ ์„ค์น˜ ์Šคํƒฌํ”„ ๋ณด์กด (Issue #8, `deploy/update.sh`)**: ์„ฑ๊ณต ๊ฒฝ๋กœ๊ฐ€ ์—…๋ฐ์ดํŠธ ์ „ `asset_hashes.txt`ยท`version.txt` ๋ฅผ ๋˜๋Œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค. ๊ทธ ๋ณต์› ํƒ“์— `version.txt` ๊ฐ€ ์ด์ „ ์ปค๋ฐ‹์„ ๊ฐ€๋ฆฌํ‚ค๊ณ  `remove.sh` ๊ฐ€ ๊ฐฑ์‹ ๋œ ์›๋ณธ ํŒŒ์ผ์„ "๋กœ์ปฌ ์ˆ˜์ •"์œผ๋กœ ์˜คํŒํ•ด ๋ฐฑ์—…ํ•˜๋˜ ๋ฌธ์ œ๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค. `install_state` ๋Š” ์žฌ์„ค์น˜๊ฐ€ ์“ฐ์ง€ ์•Š์€ ํ‚ค(์˜ˆ: `gitignore_created`)๋งŒ `key=value` ์ค„ ๋‹จ์œ„๋กœ ๋ณ‘ํ•ฉํ•˜๋ฉฐ(์ด๋ฏธ ์žˆ๋Š” ํ‚ค๋Š” ๊ฑด๋„ˆ๋œ€, ๋งˆ์ง€๋ง‰ ์ค„์— ๊ฐœํ–‰์ด ์—†์–ด๋„ ์ฒ˜๋ฆฌ), ์‹คํŒจ ๊ฒฝ๋กœ(`restore_on_failure`)๋Š” ์ข…์ „๋Œ€๋กœ ์„ธ ํŒŒ์ผ์„ ๋ชจ๋‘ ๊ทธ๋Œ€๋กœ ๋ณต์›ํ•œ๋‹ค. +4. **๊ฐ™์€ cwd ํ˜•์ œ ์›Œํฌ์ŠคํŽ˜์ด์Šค ํŒจํ‚น (Issue #7, `lib.sh` ์‰ผ `new-session`)**: ์ฒซ ๋ฒˆ์งธ๋กœ ์ผ์น˜ํ•œ pane ์˜ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋งŒ ๋ณด๋˜ ํƒ“์— ๊ฐ€๋“ ์ฐฌ ์ฒซ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ฅผ ๋งค๋ฒˆ overflow ๋กœ ํŒ์ •ํ•ด ์—์ด์ „ํŠธ๋งˆ๋‹ค ์ƒˆ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ฅผ ์—ด๋˜ ๋ฌธ์ œ(4 + 1 + 1)๋ฅผ ๊ณ ์ณค๋‹ค. ์ด์ œ ๊ฐ™์€ cwd ์˜ ๋ชจ๋“  ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ฅผ pane ์ˆœ์„œ๋Œ€๋กœ ์ˆœํšŒํ•ด idle pane ์ด ์žˆ๊ฑฐ๋‚˜ 2xK ๊ทธ๋ฆฌ๋“œ์— ์—ฌ์œ ๊ฐ€ ์žˆ๋Š” ์ฒซ ์›Œํฌ์ŠคํŽ˜์ด์Šค์— ๋ฐฐ์น˜ํ•˜๊ณ (overflow ์ธ ํ›„๋ณด๋Š” ๋‹ค์Œ ํ›„๋ณด๋กœ ๋„˜์–ด๊ฐ), ๋ชจ๋‘ ๊ฐ€๋“ ์ฐผ์„ ๋•Œ๋งŒ ์ƒˆ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ ๋‹ค(4 + 2). + +#### โœ… ํ’ˆ์งˆ ๊ฒ€์ฆ +- **์‹ ๊ทœ ํ…Œ์ŠคํŠธ 8๊ฐœ**: `tests/test_issue7_workspace_packing.py` 3๊ฐœ(์—ฌ์œ  ์žˆ๋Š” ํ˜•์ œ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋กœ ํŒจํ‚น, ๋ชจ๋“  ํ›„๋ณด๊ฐ€ ๊ฐ€๋“ ์ฐผ์„ ๋•Œ๋งŒ ์ƒˆ ์›Œํฌ์ŠคํŽ˜์ด์Šค, ์—ฌ์œ  ์žˆ๋Š” ์ฒซ ์›Œํฌ์ŠคํŽ˜์ด์Šค ์šฐ์„  โ€” mock herdr ์— ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ณ„ 2x2 ๊ธฐํ•˜๋ฅผ ํ‰๋‚ด ๋‚ด๋Š” opt-in `layout_geometry: "2x2"` ์ถ”๊ฐ€), `tests/test_issue8_update_state.py` 1๊ฐœ(์‹ค์ œ installโ†’update ์‚ฌ์ดํด์—์„œ ์ƒˆ `version.txt`ยทํ•ด์‹œ ์œ ์ง€์™€ `install_state` ๋ณ‘ํ•ฉ), `tests/test_issue12_registry_secrets.py` 4๊ฐœ(registry ๋ธ”๋ก์— ๋น„๋ฐ€๋ฒˆํ˜ธ ์—†์Œ, ๋“ฑ๋ก๋œ ๋ ˆ์ฝ”๋“œ์— ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์—†์–ด๋„ ํ™˜๊ฒฝ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ์ ‘์†, ๋ ˆ๊ฑฐ์‹œ ๋ ˆ์ฝ”๋“œ์˜ ๋‹ค์Œ ์“ฐ๊ธฐ ๋•Œ ์ •๋ฆฌ, `scrub-secrets` CLI ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜). +- **๊ธฐ์กด ํ…Œ์ŠคํŠธ ์กฐ์ •**: registry CLI ๊ณ„์•ฝ ์Šค๋ƒ…์ƒท(`tests/test_issue5_phase1_delegation.py`)๊ณผ ๋„์›€๋ง ์บก์ฒ˜(`tests/fixtures/issue_5/phase1/help-registry.txt`)์— ์˜๋„๋œ `scrub-secrets` ์„œ๋ธŒ์ปค๋งจ๋“œ๋ฅผ ๋ฐ˜์˜ํ•˜๊ณ , G6 ๊ฐ€๋“œ์— `_cmd_scrub_secrets` ๋ฅผ ์„ ์–ธ๋œ ์˜ˆ์™ธ๋กœ ์ถ”๊ฐ€. +- **์‹ค์ธก (์ปค๋ฐ‹ `e629cc1`ยท์œ„์ž„ ๋ธŒ๋ฆฌํ”„ ๊ธฐ๋ก)**: ๊ด€๋ จ ์Šค์œ„ํŠธ 325๊ฐœ์™€ ์ „์ฒด ์Šค์œ„ํŠธ(worktree ๊ธฐ์ค€) ํ†ต๊ณผ. + ### ๐Ÿš€ `v4.4.3` โ€” Issue #10ยท#13 ๋Œ€ํ™” purge ์™„๊ฒฐ, Issue #11 ๋ฉ”์‹œ์ง€ ์ •์ •, Issue #15 ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ณ ์ • (2026-09-13) > **ํŒจ์น˜ ๋งˆ์ผ์Šคํ†ค (PATCH Release)**: agy ๋Œ€ํ™” purge ๊ฐ€ resume ๊ฐ€๋Šฅํ•œ ๋ถ€์† ํŒŒ์ผ์„ ๋‚จ๊ธฐ๋˜ ๋ฌธ์ œ(Issue #10), herdr ์„ธ์…˜ ํ–‰์ด ์—†๋Š” ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ๋Œ€ํ™”๋ฅผ ์–ด๋–ค ์Šคํ‚ฌ๋กœ๋„ ์ง€์šธ ์ˆ˜ ์—†๋˜ ๋ฌธ์ œ(Issue #13), `orc_onboard.sh --remove` ์˜ "removeed" ์˜คํƒ€(Issue #11), ํ˜ธ์ถœ์ž cwd ๋ฅผ ๋”ฐ๋ผ๊ฐ€๋˜ ์žก ๋ ˆ์ง€์ŠคํŠธ๋ฆฌยท๊ฐ์‚ฌ ๋กœ๊ทธ ๋•Œ๋ฌธ์— pytest ์Šค์œ„ํŠธ๊ฐ€ ์‹ค์ œ `.mam/jobs` ์— ํ…Œ์ŠคํŠธ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋‚จ๊ธฐ๋˜ ๋ฌธ์ œ(Issue #15)๋ฅผ ํ•ด๊ฒฐํ–ˆ๋‹ค. ์ƒˆ ์„œ๋ธŒ์ปค๋งจ๋“œยท์˜ต์…˜์€ ๋ชจ๋‘ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์ด๋‹ค. ๋ ˆ์ง€์ŠคํŠธ๋ฆฌยท๊ฐ์‚ฌ ๋กœ๊ทธ์˜ **๊ธฐ๋ณธ** ๊ฒฝ๋กœ๋Š” `WORKSPACE_ROOT` ๊ฐ€ ์žˆ์œผ๋ฉด cwd ๋Œ€์‹  ๊ทธ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ฅผ ๋”ฐ๋ฅด๋ฉฐ, ๋ช…์‹œ์  `--registry-dir` ์™€ `MAM_JOBS_DIR`/`DELEGATE_JOB_LOGS_DIR` ๋Š” ๊ทธ๋Œ€๋กœ ์šฐ์„ ํ•œ๋‹ค. @@ -49,7 +64,7 @@ 5. **์žก ๋ ˆ์ง€์ŠคํŠธ๋ฆฌยท๊ฐ์‚ฌ ๋กœ๊ทธ์˜ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ณ ์ • (Issue #15, `mqtt_common.default_registry_dir`ยท`get_logs_dir`)**: ๊ธฐ๋ณธ ๊ฒฝ๋กœ๊ฐ€ `MAM_JOBS_DIR`/`DELEGATE_JOB_LOGS_DIR` โ†’ `$WORKSPACE_ROOT/.mam/jobs`ยท`$WORKSPACE_ROOT/.mam/delegate_job_logs` โ†’ cwd ๊ธฐ์ค€ `.mam/โ€ฆ` ์ˆœ์œผ๋กœ ์ •ํ•ด์ง„๋‹ค. `registry.py`ยท`publish_event.py`ยท`service.py` ์™€ `create_session.sh`ยท`stop_session.sh` ๊ฐ€ ๊ฐ™์€ ์ˆœ์„œ๋ฅผ ์“ด๋‹ค. ์ €์žฅ์†Œ ๋ฃจํŠธ์—์„œ ๋ˆ pytest ์Šค์œ„ํŠธ(๊ธฐ๋ณธ ์˜จ๋ณด๋”ฉ ์žก์„ ๋“ฑ๋กํ•˜๋Š” `create_session.sh`)๊ฐ€ ํ˜ธ์ถœ์ž cwd ๋ฅผ ๋”ฐ๋ผ ์‹ค์ œ `.mam/jobs` ์— ํ…Œ์ŠคํŠธ ๋ ˆ์ฝ”๋“œ 593๊ฑด์„ ๋‚จ๊ธฐ๋˜ ๋ฌธ์ œ๋ฅผ ํ•ด์†Œํ–ˆ๊ณ , ์ƒŒ๋“œ๋ฐ•์Šค ํ”ฝ์Šค์ฒ˜(`tests/conftest.py`)๋Š” ๋‘ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ๋ชจ๋‘ ๊ณ ์ •ํ•œ๋‹ค. #### โœ… ํ’ˆ์งˆ ๊ฒ€์ฆ -- **์‹ ๊ทœ ํ…Œ์ŠคํŠธ 24๊ฐœ**: `tests/test_issue10_13_purge.py` 17๊ฐœ(agy ๋ถ€์† ํŒŒ์ผยทannotationยทlock ์‚ญ์ œ, ์‚ฌ์šฉ ์ค‘ lock ๋ณด์กด, ์กด์žฌํ•œ ๊ฒƒ๋งŒ ๋ณด๊ณ , ์ž๊ธฐ ๊ณ„๋ณด๋ฅผ ๋บ€ in-use ํŒ์ •, ๋น„-์—์ด์ „ํŠธ ํ—ฌํผ ์‚ฌ์Šฌ๋งŒ ์ œ์™ธํ•˜๊ณ  ์—์ด์ „ํŠธ CLI ์กฐ์ƒ์€ ๊ฒ€์‚ฌ, purge CLI ์˜ auto ํŒ๋ณ„ยท์‚ฌ์šฉ ์ค‘ ๊ฑฐ๋ถ€ยท์ž๊ธฐ ์—์ด์ „ํŠธ ์„ธ์…˜ ์•ˆ์—์„œ ๊ฑฐ๋ถ€ยท์„ธ์…˜ id ํ™˜๊ฒฝ๋ณ€์ˆ˜ 5์ข…์œผ๋กœ ๊ฑฐ๋ถ€ยท๋ฏธํ•ด๊ฒฐ uuid, `orc_onboard` purge ์˜ `--yes` ์š”๊ตฌยท์‚ญ์ œ ํ›„ ๋“ฑ๋ก ํ•ด์ œยท์‚ฌ์šฉ ์ค‘ ๊ฑฐ๋ถ€ยท์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ์ž๊ธฐ ์„ธ์…˜์—์„œ ๊ฑฐ๋ถ€ยทartifact ์—†์Œ์—๋„ ๋“ฑ๋ก ํ•ด์ œยท`--remove` ํ•„์ˆ˜ยทdry-run ๋ฌด๋ณ€๊ฒฝ), `tests/test_issue15_registry_isolation.py` 7๊ฐœ(`MAM_JOBS_DIR` ์šฐ์„ , `WORKSPACE_ROOT` ์ถ”์ข…, cwd ํด๋ฐฑ, ๋กœ๊ทธ ๊ฒฝ๋กœ ๋™์ผ ์ˆœ์„œ, CLI ๊ธฐ๋ณธ๊ฐ’์˜ ํ”„๋กœ์„ธ์Šค๋ณ„ ํ•ด์„, ๋ช…์‹œ env ๋กœ ์˜จ๋ณด๋”ฉ ์žก ์ƒŒ๋“œ๋ฐ•์Šค ์œ ์ง€, `WORKSPACE_ROOT` ๋งŒ์œผ๋กœ๋„ ์ƒŒ๋“œ๋ฐ•์Šค ์œ ์ง€). +- **์‹ ๊ทœ ํ…Œ์ŠคํŠธ 26๊ฐœ**: `tests/test_issue10_13_purge.py` 19๊ฐœ(agy ๋ถ€์† ํŒŒ์ผยทannotationยทlock ์‚ญ์ œ, ์‚ฌ์šฉ ์ค‘ lock ๋ณด์กด, ์กด์žฌํ•œ ๊ฒƒ๋งŒ ๋ณด๊ณ , ์ž๊ธฐ ๊ณ„๋ณด๋ฅผ ๋บ€ in-use ํŒ์ •, ๋น„-์—์ด์ „ํŠธ ํ—ฌํผ ์‚ฌ์Šฌ๋งŒ ์ œ์™ธํ•˜๊ณ  ์—์ด์ „ํŠธ CLI ์กฐ์ƒ์€ ๊ฒ€์‚ฌ, purge CLI ์˜ auto ํŒ๋ณ„ยท์‚ฌ์šฉ ์ค‘ ๊ฑฐ๋ถ€ยท์ž๊ธฐ ์—์ด์ „ํŠธ ์„ธ์…˜ ์•ˆ์—์„œ ๊ฑฐ๋ถ€ยท์„ธ์…˜ id ํ™˜๊ฒฝ๋ณ€์ˆ˜ 5์ข…์œผ๋กœ ๊ฑฐ๋ถ€ยท๋ฏธํ•ด๊ฒฐ uuid, `orc_onboard` purge ์˜ `--yes` ์š”๊ตฌยท์‚ญ์ œ ํ›„ ๋“ฑ๋ก ํ•ด์ œยท์‚ฌ์šฉ ์ค‘ ๊ฑฐ๋ถ€ยท์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ์ž๊ธฐ ์„ธ์…˜์—์„œ ๊ฑฐ๋ถ€ยทartifact ์—†์Œ์—๋„ ๋“ฑ๋ก ํ•ด์ œยท`--remove` ํ•„์ˆ˜ยทdry-run ๋ฌด๋ณ€๊ฒฝ, presence lock ๋งŒ ์—ฐ ๋ณด์œ ์ž ๊ฑฐ๋ถ€, DB ๊ฐ€ ์—†๋Š” ๊ณ ์•„ ๋ถ€์† ํŒŒ์ผ๋กœ agy ํ•ด๊ฒฐยทpurge), `tests/test_issue15_registry_isolation.py` 7๊ฐœ(`MAM_JOBS_DIR` ์šฐ์„ , `WORKSPACE_ROOT` ์ถ”์ข…, cwd ํด๋ฐฑ, ๋กœ๊ทธ ๊ฒฝ๋กœ ๋™์ผ ์ˆœ์„œ, CLI ๊ธฐ๋ณธ๊ฐ’์˜ ํ”„๋กœ์„ธ์Šค๋ณ„ ํ•ด์„, ๋ช…์‹œ env ๋กœ ์˜จ๋ณด๋”ฉ ์žก ์ƒŒ๋“œ๋ฐ•์Šค ์œ ์ง€, `WORKSPACE_ROOT` ๋งŒ์œผ๋กœ๋„ ์ƒŒ๋“œ๋ฐ•์Šค ์œ ์ง€). - **๊ธฐ์กด ํ…Œ์ŠคํŠธ ์กฐ์ •**: O-13(`tests/test_orc_onboard.py`)์ด ์ •์ •๋œ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹จ์ •ํ•˜๊ณ , R1 ํ—ฌํผยทB-9 ๋กœ๊ทธ ๊ฒฝ๋กœ ํ…Œ์ŠคํŠธ์™€ `publish_event` ์Šค๋ƒ…์ƒท(`tests/test_issue5_phase1_delegation.py`ยท`tests/test_tier1_unit.py`)์ด ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ณ ์ • ๊ณ„์•ฝ์— ๋งž๊ฒŒ ์กฐ์ •๋จ. - **์‹ค์ธก (์ปค๋ฐ‹ `c82ea75`ยท์œ„์ž„ ๋ธŒ๋ฆฌํ”„ ๊ธฐ๋ก)**: ์ „์ฒด ์Šค์œ„ํŠธ 839/839 ํ†ต๊ณผ, ์Šค์œ„ํŠธ ์‹คํ–‰ ์ „ํ›„ ์‹ค์ œ `.mam/jobs` ๋ ˆ์ฝ”๋“œ 881๊ฑด ๋ถˆ๋ณ€. diff --git a/deploy/update.sh b/deploy/update.sh index 645da06..48b1836 100644 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -229,11 +229,33 @@ if [ $HAS_MAM -eq 1 ]; then mkdir -p .mam/delegate_job_logs cp -rf .mam.update-tmp/delegate_job_logs/* .mam/delegate_job_logs/ fi - for st in install_state asset_hashes.txt version.txt; do - if [ -f ".mam.update-tmp/$st" ]; then - cp -f ".mam.update-tmp/$st" .mam/ + # Issue #8: asset_hashes.txt and version.txt describe the tree the + # installer just wrote, so the pre-update copies must NOT come back here - + # restoring them made version.txt name the previous commit and remove.sh + # treat every updated file as locally modified (spurious backups). Only + # install_state carries history worth keeping: merge in any key the fresh + # install did not write (e.g. gitignore_created from the original install). + # The failure path (restore_on_failure) still puts all three back verbatim. + if [ -f ".mam.update-tmp/install_state" ]; then + touch .mam/install_state + # A fresh install_state without a final newline would glue the first + # merged key onto its last line. + if [ -s .mam/install_state ] && [ -n "$(tail -c1 .mam/install_state)" ]; then + printf '\n' >> .mam/install_state fi - done + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + *=*) ;; + *) continue ;; # not a key=value line + esac + key="${line%%=*}" + [ -n "$key" ] || continue + # exact key match, no regex interpretation of the key + if ! awk -F= -v k="$key" '$1 == k { found = 1 } END { exit !found }' .mam/install_state; then + printf '%s\n' "$line" >> .mam/install_state + fi + done < .mam.update-tmp/install_state + fi if [ -d ".mam.update-tmp/skill-backups" ]; then mkdir -p .mam/skill-backups cp -rf .mam.update-tmp/skill-backups/* .mam/skill-backups/ 2>/dev/null || true diff --git a/tests/conftest.py b/tests/conftest.py index 5164733..93f144d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -334,15 +334,50 @@ elif cmd1 == "pane": focused = "w1:p1" panes_list = [] agents = state.get("agents", {}) - count = max(len(agents), 1) - w_per_pane = 184 // count - idx = 0 - for name, data in agents.items(): - panes_list.append({ - "pane_id": data.get("pane_id", f"w1:p{idx+1}"), - "rect": {"width": w_per_pane, "height": 78} - }) - idx += 1 + if state.get("layout_geometry") == "2x2": + # Issue #7 tests: real per-workspace geometry. Only the panes of + # the sampled pane's workspace are reported, laid out as the 2x2 + # grid herdr builds, so lib_py.layout judges a full workspace as + # overflow while a sibling workspace with room is offered a split. + sample = "" + i = 2 + while i < len(args): + if args[i] == "--pane" and i + 1 < len(args): + sample = args[i+1] + i += 2 + else: + i += 1 + ws_of = None + for data in agents.values(): + if data.get("pane_id") == sample: + ws_of = data.get("workspace_id") + break + if ws_of is None and ":" in sample: + ws_of = sample.split(":")[0] + members = [d for d in agents.values() if ws_of is None or d.get("workspace_id", "w1") == ws_of] + n = len(members) + for idx, data in enumerate(members): + if n == 1: + rect = {"x": 0, "y": 0, "width": 184, "height": 78} + elif n == 2: + rect = {"x": 92 * idx, "y": 0, "width": 92, "height": 78} + elif n == 3: + rect = [{"x": 0, "y": 0, "width": 92, "height": 39}, + {"x": 0, "y": 39, "width": 92, "height": 39}, + {"x": 92, "y": 0, "width": 92, "height": 78}][idx] + else: + rect = {"x": 92 * (idx % 2), "y": 39 * (idx // 2), "width": 92, "height": 39} + panes_list.append({"pane_id": data.get("pane_id", f"w1:p{idx+1}"), "rect": rect}) + else: + count = max(len(agents), 1) + w_per_pane = 184 // count + idx = 0 + for name, data in agents.items(): + panes_list.append({ + "pane_id": data.get("pane_id", f"w1:p{idx+1}"), + "rect": {"width": w_per_pane, "height": 78} + }) + idx += 1 if not panes_list: panes_list = [{"pane_id": "w1:p1", "rect": {"width": 184, "height": 78}}] print(json.dumps({ diff --git a/tests/fixtures/issue_5/phase1/help-registry.txt b/tests/fixtures/issue_5/phase1/help-registry.txt index 3d63ce0..6a660e8 100644 --- a/tests/fixtures/issue_5/phase1/help-registry.txt +++ b/tests/fixtures/issue_5/phase1/help-registry.txt @@ -1,11 +1,11 @@ usage: registry.py [-h] [--registry-dir REGISTRY_DIR] - {register,list,get,status,update,close,get-feedback,pick,logs} + {register,list,get,status,update,close,get-feedback,pick,logs,scrub-secrets} ... multi-agent-mux-delegate-job registry CLI positional arguments: - {register,list,get,status,update,close,get-feedback,pick,logs} + {register,list,get,status,update,close,get-feedback,pick,logs,scrub-secrets} register create a pending job; prints the job id list list jobs (optionally by status) get print one job record as JSON @@ -17,6 +17,8 @@ positional arguments: pick claim a pending job for a session; prints id logs show the persistent audit log for a job, or --list every logged job + scrub-secrets remove broker.password from every job record (Issue + #12 migration) optional arguments: -h, --help show this help message and exit diff --git a/tests/test_issue12_registry_secrets.py b/tests/test_issue12_registry_secrets.py new file mode 100644 index 0000000..16142f5 --- /dev/null +++ b/tests/test_issue12_registry_secrets.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Issue #12 โ€” the broker password must not be persisted in job records. + +Every sub-agent can read `.mam/jobs/.json`, so `MQTT_PASSWORD` copied +there ends up in agent transcripts. The broker block now carries only +host/port/tls/username; the password is layered back from the environment +(`.mam.env`) at connect time. Records written before this rule are cleaned on +their next write and by `registry.py scrub-secrets`. +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +SKILLS = REPO_ROOT / ".agents" / "skills" +REGISTRY_PY = SKILLS / "multi-agent-mux-delegate-job" / "scripts" / "registry.py" + +sys.path.insert(0, str(SKILLS)) +from lib_py.delegation import mqtt_common, registry # noqa: E402 + + +def _secure_env(monkeypatch): + monkeypatch.setenv("MQTT_BROKER", "broker.example.test") + monkeypatch.setenv("MQTT_USERNAME", "mam-user") + monkeypatch.setenv("MQTT_PASSWORD", "s3cret-broker-pass") + + +def test_registry_block_carries_no_password(monkeypatch): + _secure_env(monkeypatch) + block = mqtt_common.broker_config_from_env().to_registry_block() + assert "password" not in block + assert block["username"] == "mam-user" and block["host"] == "broker.example.test" + + +def test_registered_record_has_no_password_but_connects_with_it(mam_sandbox, monkeypatch): + _secure_env(monkeypatch) + rd = str(mam_sandbox / ".mam" / "jobs") + job_id = registry.register_job("issue 12", "claude-code", "herdr:claude", registry_dir=rd) + + record = json.loads((Path(rd) / f"{job_id}.json").read_text()) + assert "password" not in record["broker"], record["broker"] + assert "s3cret-broker-pass" not in json.dumps(record) + # The secure broker still gets its per-job HMAC key (needed by the agent's + # own publish_event to sign; documented as remaining in the record). + assert record["auth_token"] + + cfg = mqtt_common.broker_config_from_job(record) + assert cfg.password == "s3cret-broker-pass", "password must come back from the environment" + assert cfg.username == "mam-user" + + +def test_legacy_record_is_scrubbed_on_next_write(mam_sandbox, monkeypatch): + _secure_env(monkeypatch) + rd = str(mam_sandbox / ".mam" / "jobs") + job_id = registry.register_job("legacy", "claude-code", "herdr:claude", registry_dir=rd) + path = Path(rd) / f"{job_id}.json" + record = json.loads(path.read_text()) + record["broker"]["password"] = "leaked-earlier" + path.write_text(json.dumps(record)) + + registry.update_status(job_id, rd, "running") + + after = json.loads(path.read_text()) + assert after["status"] == "running" + assert "password" not in after["broker"] + + +def test_scrub_secrets_cli_migrates_a_registry(mam_sandbox, monkeypatch): + _secure_env(monkeypatch) + rd = str(mam_sandbox / ".mam" / "jobs") + dirty = registry.register_job("dirty", "claude-code", "herdr:claude", registry_dir=rd) + clean = registry.register_job("clean", "claude-code", "herdr:claude", registry_dir=rd) + dirty_path = Path(rd) / f"{dirty}.json" + rec = json.loads(dirty_path.read_text()) + rec["broker"]["password"] = "leaked-earlier" + dirty_path.write_text(json.dumps(rec)) + clean_before = (Path(rd) / f"{clean}.json").read_text() + # What the old to_registry_block wrote for every unauthenticated job: the key + # with a null value. No secret, so the migration must neither report nor + # rewrite it (a real PoC registry held ~900 of these and no password at all). + empty = registry.register_job("empty-key", "claude-code", "herdr:claude", registry_dir=rd) + empty_path = Path(rd) / f"{empty}.json" + rec_empty = json.loads(empty_path.read_text()) + rec_empty["broker"]["password"] = None + empty_path.write_text(json.dumps(rec_empty)) + empty_before = empty_path.read_text() + + env = dict(os.environ, PYTHONPATH=f"{SKILLS}:{os.environ.get('PYTHONPATH', '')}") + dry = subprocess.run([sys.executable, str(REGISTRY_PY), "--registry-dir", rd, "scrub-secrets", "--dry-run"], + capture_output=True, text=True, env=env, cwd=str(mam_sandbox)) + assert dry.returncode == 0, dry.stderr + assert f"would scrub 1 record(s)" in dry.stdout and dirty in dry.stdout + assert empty not in dry.stdout, "an empty password key is not a stored secret" + assert "leaked-earlier" in dirty_path.read_text(), "--dry-run must not write" + + res = subprocess.run([sys.executable, str(REGISTRY_PY), "--registry-dir", rd, "scrub-secrets"], + capture_output=True, text=True, env=env, cwd=str(mam_sandbox)) + assert res.returncode == 0, res.stderr + assert "scrubbed 1 record(s)" in res.stdout + scrubbed = json.loads(dirty_path.read_text()) + assert "password" not in scrubbed["broker"] + assert scrubbed["prompt"] == "dirty" and scrubbed["auth_token"] == rec["auth_token"] + assert (Path(rd) / f"{clean}.json").read_text() == clean_before, "untouched records are not rewritten" + assert empty_path.read_text() == empty_before, "a null password key is left for the record's next write" diff --git a/tests/test_issue14_delivery_ack.py b/tests/test_issue14_delivery_ack.py index a21042f..be0e967 100644 --- a/tests/test_issue14_delivery_ack.py +++ b/tests/test_issue14_delivery_ack.py @@ -326,7 +326,7 @@ def test_wrapper_idle_timeout_grace_accepts_disk_heartbeat(tmp_path): "--prompt", "issue 14 disk heartbeat", "--registry-dir", str(reg_dir), "--workdir", str(tmp_path), - "--timeout", "60", + "--timeout", "90", "--idle-timeout", "6", ], cwd=str(tmp_path), env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, @@ -341,20 +341,33 @@ def test_wrapper_idle_timeout_grace_accepts_disk_heartbeat(tmp_path): time.sleep(0.1) assert jid, "job was not registered" + # Heartbeats are fired without waiting for the wire publish: publish_event + # records the event in events.log before it touches the broker (ebe58f6), + # and that on-disk timestamp is what the wrapper's grace reads. Blocking on + # each publish (2-6 s against the public broker, with retries) spaced the + # disk records further apart than the 6 s idle window and the job was + # closed before the next heartbeat landed - a broker-latency flake, not a + # wrapper defect. An agent's own heartbeat timer behaves the same way. + beats = [] + def heartbeat(detail: str): - subprocess.run( + beats.append(subprocess.Popen( [sys.executable, str(PUBLISH_PY), "--registry-dir", str(reg_dir), "--job", jid, "--event", "progress", "--detail", detail], - capture_output=True, text=True, cwd=str(tmp_path), env=env, - ) + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=str(tmp_path), env=env, + )) - # Subscriber idles out at ~6 s (plus broker connect); heartbeats then keep - # arriving 2 s apart, well inside the 6 s window each one re-opens (the - # wrapper's poll iteration costs ~2 s of herdr RPCs, so the ratio matters). + # Subscriber idles out at ~6 s plus the broker connect (the public broker + # has taken 5-10 s); heartbeats then keep landing 2 s apart for 20 s, well + # inside the 6 s window each one re-opens (the wrapper's poll iteration + # costs ~2 s of herdr RPCs, so the ratio matters) and long enough to + # outlast a slow connect. time.sleep(9) - for i in range(5): + for i in range(10): heartbeat(f"still working {i}") time.sleep(2) + for b in beats: + b.wait(timeout=60) done = subprocess.run( [sys.executable, str(REGISTRY_PY), "--registry-dir", str(reg_dir), "update", "--job", jid, "--status", "completed"], @@ -362,7 +375,7 @@ def test_wrapper_idle_timeout_grace_accepts_disk_heartbeat(tmp_path): ) assert done.returncode == 0, done.stderr - stdout, stderr = proc.communicate(timeout=90) + stdout, stderr = proc.communicate(timeout=150) job_data = json.loads((reg_dir / f"{jid}.json").read_text()) assert job_data.get("status") == "completed", f"status={job_data.get('status')}\n{stdout}\n{stderr}" assert proc.returncode == 0, f"expected rc=0 while heartbeats kept landing, got {proc.returncode}\n{stdout}\n{stderr}" diff --git a/tests/test_issue5_phase1_delegation.py b/tests/test_issue5_phase1_delegation.py index 32c851a..dd08c8e 100644 --- a/tests/test_issue5_phase1_delegation.py +++ b/tests/test_issue5_phase1_delegation.py @@ -330,6 +330,7 @@ def test_registry_cli_contract_snapshot(mam_sandbox): commands = set(subparsers[0].choices) assert commands == { "register", "list", "get", "status", "update", "close", "get-feedback", "pick", "logs", + "scrub-secrets", # Issue #12 migration }, commands reg = _parser_snapshot(subparsers[0].choices["register"]) assert reg["--prompt"][2] is None diff --git a/tests/test_issue5_phase2a_loop.py b/tests/test_issue5_phase2a_loop.py index 3149f61..1837849 100644 --- a/tests/test_issue5_phase2a_loop.py +++ b/tests/test_issue5_phase2a_loop.py @@ -339,6 +339,8 @@ def test_g6_every_record_writer_is_the_engine_or_a_declared_exception(): ("mqtt_common.py", "_update_job_status_legacy"), ("registry.py", "_pick_pending_legacy"), ("engine.py", "transition"), + # Issue #12 migration: drops broker.password only, never status + ("registry.py", "_cmd_scrub_secrets"), } found = set() for path in list((SKILLS / "lib_py" / "delegation").glob("*.py")) + \ diff --git a/tests/test_issue7_workspace_packing.py b/tests/test_issue7_workspace_packing.py new file mode 100644 index 0000000..0c49f50 --- /dev/null +++ b/tests/test_issue7_workspace_packing.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Issue #7 โ€” a new agent must be packed into any same-cwd workspace that still +has room in its 2xK grid; a fresh workspace is created only when all of them +are full. + +The shim's `new-session` used to take the first pane whose cwd matched, which +was always the (full) first workspace, judged it overflow and opened yet +another workspace: six agents came out as 4 + 1 + 1 instead of 4 + 2. + +The mock herdr reports real per-workspace geometry when the state carries +``layout_geometry: "2x2"``, so lib_py.layout sees a full workspace as +overflow and a half-empty sibling as splittable. +""" +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +CWD_AGENTS = { + # workspace w1: full 2x2 grid + "a1": ("w1", "w1:p1"), "a2": ("w1", "w1:p2"), "a3": ("w1", "w1:p3"), "a4": ("w1", "w1:p4"), +} + + +def _seed(state_file: Path, sandbox: Path, layout: dict) -> None: + """layout: workspace id -> number of agents already in it (same cwd).""" + state = json.loads(state_file.read_text()) + state["layout_geometry"] = "2x2" + agents = {} + workspaces = [] + for ws_id, count in layout.items(): + workspaces.append({"workspace_id": ws_id, "label": "default", "cwd": str(sandbox)}) + for i in range(1, count + 1): + agents[f"{ws_id}-agent-{i}"] = { + "agent": "claude", + "agent_status": "idle", + "status": "running", + "workspace_id": ws_id, + "pane_id": f"{ws_id}:p{i}", + "cwd": str(sandbox), + "buffer": "Ready", + } + state["workspaces"] = workspaces + state["agents"] = agents + state_file.write_text(json.dumps(state, indent=2)) + + +def _new_session(sandbox: Path, name: str) -> subprocess.CompletedProcess: + lib = sandbox / ".agents" / "skills" / "lib.sh" + script = f''' + source "{lib}" + _init_herdr_isolation + herdr new-session -d -s "{name}" -c "{sandbox}" "claude --dangerously-skip-permissions" + ''' + return subprocess.run(["bash", "-c", script], capture_output=True, text=True, cwd=str(sandbox), timeout=180) + + +def _calls(state_file: Path, *prefix: str) -> list: + calls = json.loads(state_file.read_text()).get("calls", []) + return [c for c in calls if c[:len(prefix)] == list(prefix)] + + +def test_agent_packs_into_sibling_workspace_with_room(mam_sandbox, mock_herdr, mock_agents): + """w1 holds 4 agents (full), w2 holds 1: the sixth agent must split w2.""" + state_file = Path(mock_herdr) + _seed(state_file, mam_sandbox, {"w1": 4, "w2": 1}) + + res = _new_session(mam_sandbox, "sixth-creator-claude") + assert res.returncode == 0, res.stderr + res.stdout + + splits = _calls(state_file, "pane", "split") + assert splits, "the sixth agent must be placed by splitting a pane" + assert all("HERDR_WORKSPACE_ID=w2" in c and "--pane" in c and c[c.index("--pane") + 1].startswith("w2:") + for c in splits), splits + assert not _calls(state_file, "workspace", "create"), "no third workspace while w2 has room" + + +def test_new_workspace_only_when_every_candidate_is_full(mam_sandbox, mock_herdr, mock_agents): + """w1 and w2 both hold 4 agents: only then is a third workspace created.""" + state_file = Path(mock_herdr) + _seed(state_file, mam_sandbox, {"w1": 4, "w2": 4}) + + res = _new_session(mam_sandbox, "ninth-creator-claude") + assert res.returncode == 0, res.stderr + res.stdout + + assert not _calls(state_file, "pane", "split"), "a full grid must not be split" + assert len(_calls(state_file, "workspace", "create")) == 1 + + +def test_idle_pane_in_a_later_workspace_is_reused(mam_sandbox, mock_herdr, mock_agents): + """w1 is full and w2 keeps an agent-less pane behind (BUG-4 reuse): the + candidate walk must land on that idle pane - no split, no new workspace.""" + state_file = Path(mock_herdr) + _seed(state_file, mam_sandbox, {"w1": 4, "w2": 1}) + state = json.loads(state_file.read_text()) + state.setdefault("panes", []).append({"pane_id": "w2:p_idle", "workspace_id": "w2", "cwd": str(mam_sandbox), "tab_id": "w2:t1"}) + state_file.write_text(json.dumps(state, indent=2)) + + res = _new_session(mam_sandbox, "seventh-creator-claude") + assert res.returncode == 0, res.stderr + res.stdout + + assert not _calls(state_file, "pane", "split"), "an idle pane must be reused, not split around" + assert not _calls(state_file, "workspace", "create") + starts = _calls(state_file, "agent", "start") + assert starts and "w2:p_idle" in starts[-1], starts + + +def test_ws_label_rename_targets_only_the_chosen_workspace(mam_sandbox, mock_herdr, mock_agents): + state_file = Path(mock_herdr) + _seed(state_file, mam_sandbox, {"w1": 4, "w2": 1}) + lib = mam_sandbox / ".agents" / "skills" / "lib.sh" + script = f''' + source "{lib}" + _init_herdr_isolation + MAM_WS_LABEL="team-b" herdr new-session -d -s "eighth-creator-claude" -c "{mam_sandbox}" "claude --dangerously-skip-permissions" + ''' + res = subprocess.run(["bash", "-c", script], capture_output=True, text=True, cwd=str(mam_sandbox), timeout=180) + assert res.returncode == 0, res.stderr + res.stdout + + renames = _calls(state_file, "workspace", "rename") + assert renames == [["workspace", "rename", "w2", "team-b"]], renames + + +def test_first_workspace_with_room_still_wins(mam_sandbox, mock_herdr, mock_agents): + """Regression guard: with a single half-empty workspace the agent lands + there, exactly as before.""" + state_file = Path(mock_herdr) + _seed(state_file, mam_sandbox, {"w1": 2}) + + res = _new_session(mam_sandbox, "third-creator-claude") + assert res.returncode == 0, res.stderr + res.stdout + + splits = _calls(state_file, "pane", "split") + assert len(splits) == 1 and "HERDR_WORKSPACE_ID=w1" in splits[0], splits + assert not _calls(state_file, "workspace", "create") diff --git a/tests/test_issue8_update_state.py b/tests/test_issue8_update_state.py new file mode 100644 index 0000000..e572e01 --- /dev/null +++ b/tests/test_issue8_update_state.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Issue #8 โ€” update.sh must keep the installer's fresh version.txt and +asset_hashes.txt instead of restoring the pre-update copies. + +Restoring them left version.txt naming the previous commit and made remove.sh +compare against stale hashes, so every updated-but-pristine file was backed up +as "locally modified". install_state is the one file whose old keys are +merged back (gitignore_created from the original install). +""" +from __future__ import annotations + +import os +import shutil +import subprocess +import tempfile +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent + + +@pytest.fixture +def installed_workspace(): + temp = tempfile.mkdtemp(prefix="mam_issue8_") + ws = Path(temp) / "workspace" + ws.mkdir() + env = os.environ.copy() + env["MAM_REPO_URL"] = str(REPO_ROOT) + env["MAM_SKIP_VENV"] = "1" + env["MAM_INSTALLER_URL"] = "file://" + str(REPO_ROOT / "deploy" / "install.sh") + res = subprocess.run(["bash", str(REPO_ROOT / "deploy" / "install.sh"), str(ws)], + env=env, capture_output=True, text=True, timeout=600) + assert res.returncode == 0, res.stderr + try: + yield ws, env + finally: + shutil.rmtree(temp, ignore_errors=True) + + +def test_update_keeps_fresh_version_and_hashes_and_merges_install_state(installed_workspace): + ws, env = installed_workspace + mam = ws / ".mam" + fresh_version = (mam / "version.txt").read_text() + assert "commit=" in fresh_version + + # Simulate the previous install: stale stamps and an install_state key the + # re-install will not write again. + (mam / "version.txt").write_text("source=old\ncommit=stale0000\nfetched_at=then\nmethod=git\n") + (mam / "asset_hashes.txt").write_text("stale-hash-db\n") + with open(mam / "install_state", "a") as fh: + fh.write("custom_marker=kept\n") + + res = subprocess.run(["bash", str(ws / ".mam_deploy" / "update.sh"), "--force"], + cwd=str(ws), env=env, capture_output=True, text=True, timeout=600) + assert res.returncode == 0, res.stderr + res.stdout + + version = (mam / "version.txt").read_text() + assert "stale0000" not in version, version + assert "commit=" in version and version.strip() != "" + hashes = (mam / "asset_hashes.txt").read_text() + assert "stale-hash-db" not in hashes and hashes.strip(), "asset_hashes.txt must be the installer's fresh ledger" + + state = (mam / "install_state").read_text() + assert "custom_marker=kept" in state, state + assert state.count("gitignore_created=1") == 1, "merged keys must not be duplicated:\n" + state