Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions plugins/preview-forge/agents/meta/chief-engineer-pm.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,30 @@ esac
`mode=inline`은 정상 분기이며 helper는 exit 0을 반환한다 (swap은 에러가 아니라 기대된 alternative path). `mode=error`만 helper 자체가 비-0 exit code로 propagate한다. 회귀 테스트: `tests/fixtures/h1-modal-swap/verify.sh`가 PATH-stripped 환경(`open`/`xdg-open`/`powershell.exe`/`pwsh` 부재)에서 byte-equal `{"mode":"inline",...}` 출력을, 가짜 `open` shim 환경에서 byte-equal `{"mode":"browser",...}` 출력을 어설션한다.
<!-- end A-5 -->

<!-- H1→SpecDD auto-advance (PR Phase 1, addresses user-reported gap) -->
#### §3.9 — H1 잠금 직후 SpecDD 자동 dispatch (필수, 자동, 사용자 입력 없음)

`design-approved.json`이 잠금된 직후 (= `chosen_preview.json.lock` + `design-approved.json` 모두 존재), M3는 **사용자 추가 입력 없이 즉시** SpecDD 사이클을 dispatch한다. 이는 README의 "human clicks twice" 약속의 핵심 — H1과 H2 외에는 자동 진행이어야 한다.

검증 스크립트:
```bash
bash scripts/dispatch-spec-cycle.sh runs/<id>/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Call dispatch validator via plugin-root absolute path

The new imperative uses bash scripts/dispatch-spec-cycle.sh runs/<id>/, but Preview Forge sessions run from user workspaces (created by pf init) where scripts/ is not present, and M1 pre-flight explicitly avoids operating inside the plugin repo. In that normal runtime context this command becomes No such file or directory, so H1→SpecDD auto-advance cannot produce the dispatch JSON. Use the same ${CLAUDE_PLUGIN_ROOT}/../../scripts/... form already used in this file for other scripts.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지적 정확합니다. 사용자 workspace(pf init로 생성된 환경)에는 plugin repo의 scripts/ 디렉터리가 없으므로 bare scripts/dispatch-spec-cycle.sh 호출은 No such file or directory로 실패합니다.

§3.9 검증 스크립트 호출을 같은 파일의 다른 §3 helper들(generate-gallery.sh, open-browser.sh, h1-modal-helper.sh, generate-spec-anchor-audit.py)과 동일한 ${CLAUDE_PLUGIN_ROOT}/../../scripts/... 절대 경로 형태로 통일했습니다 (commit 3d2c75b).

전체 회귀 suite (verify-plugin / security / e2e standard·pro·max / advocate-boilerplate / lesson07 / cache-concurrency 3종 / filled-ratio-gating / h1-modal-swap / spec-anchor-convergence) 모두 그린 확인했습니다.

# exit 0 + JSON {"action":"dispatch",...} → 즉시 다음 단계
# exit 2 → 락 산출물 누락; 사용자에게 H1 미완료 보고
```

dispatch JSON이 출력되면 M3는 즉시 다음 Task를 호출한다:
```
Task({
subagent_type: "pf:spec:spec-lead",
description: "SpecDD cycle start (post-H1 auto)",
prompt: "runs/<id>/ 락 산출물(chosen_preview.json.lock + design-approved.json + idea.spec.json)을 입력으로 OpenAPI v1을 작성한다. SC1-SC7 7개 critic을 순차 dispatch하여 합의된 spec을 specs/openapi.yaml + .lock으로 잠근다."
})
```

이 dispatch는 markdown 지시가 아니라 **명령형 imperative** — LLM trust 줄이기 위해 의도적으로 명시적 Task block.
<!-- end H1→SpecDD auto-advance -->

### 4. Memory 파일 관리 (쓰기 권한 독점)

**Rule 3**에 따라 당신만 `memory/{CLAUDE,PROGRESS,LESSONS}.md`에 쓸 수 있습니다. 다른 agent는 Blackboard에 `memory.request.{file}` 키로 요청 → 당신이 검토 후 batch 반영.
Expand Down
21 changes: 21 additions & 0 deletions plugins/preview-forge/agents/meta/run-supervisor.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,27 @@ CLI에서 `scripts/pre-flight.sh` 또는 `pf check`가 동일 검증을 수동
- 각 사이클 완료 조건(산출물 해시·잠금 파일) 검증 후 다음 진입
- Profile에 따라 Advocate 수 (9/18/26), Engineering 팀 수 (2×5/3×5/5×5), SCC iter (3/4/5), Panel 모드 자동 설정

<!-- H1 sentinel polling (PR Phase 1) -->
#### Sentinel-driven 자동 cycle 진행 (run-supervisor → M3 dispatch)

매 standup tick마다 다음 두 sentinel 파일을 polling한다:

1. `runs/*/.h1-frozen-signal` — `post-h1-signal.py` hook이 작성. SpecDD 시작 시그널.
- 발견 시: M3에 `dispatch_spec_cycle(run_id)` 알림 → M3가 §3.9 절차 수행.
- 처리 후 `runs/.last-spec-dispatch` touch로 idempotent 보장.

2. `runs/*/.h2-frozen-signal` — (Phase 2에서 추가 예정. 현재는 placeholder.)

polling 명령:
```bash
# 첫 polling 전에 watermark 파일을 epoch=0으로 초기 시드 (fresh install 시
# `runs/.last-spec-dispatch`가 없으면 `find -newer`가 0 result + exit 1을
# 반환해 첫 H1 signal을 invisible하게 만든다 — codex P1 수정).
[ -f runs/.last-spec-dispatch ] || { mkdir -p runs && touch -t 197001010000 runs/.last-spec-dispatch; }
find runs/ -maxdepth 2 -name '.h1-frozen-signal' -newer runs/.last-spec-dispatch 2>/dev/null
```
<!-- end H1 sentinel polling -->

### 2. Memory pre-load
새 run 시작 시 관련 LESSONS 항목을 추출하여 각 department lead의 system prompt에 동적으로 주입:
- I_LEAD에게는 PreviewDD·Mockup 관련 LESSONS
Expand Down
2 changes: 1 addition & 1 deletion plugins/preview-forge/commands/new.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ CLI 환경에서는 `scripts/pre-flight.sh` 또는 `pf check`로 동일 검증
- escalation: advocate vote dispersion > confidence_threshold → 자동으로 full panel로 복귀
10. Mitigation Designer가 dissent → action items 변환
11. Gate H1(`/pf:design`) 자동 호출: `scripts/generate-gallery.sh` + `scripts/open-browser.sh`가 먼저 뜨며 `runs/<id>/mockups/gallery.html`을 브라우저에 띄우고, 동시에 AskUserQuestion으로 preview 선택 수집 → `chosen_preview.json` 잠금
12. 사용자 디자인 승인 후 SpecDD cycle 시작 (이후 `idea-drift-detector.py` 훅이 모든 spec write를 containment 검사 — v1.6.0에서도 Rule 9 anchor는 `chosen_preview`만 유지한다. `idea.spec.json`은 advocate ground truth + PreviewDD cache key에만 사용되며, 기술 spec 파일에 business vocab이 없을 때 false-positive를 피하기 위해 drift 계산 대상에서 제외됨.)
12. 사용자가 H1에서 디자인을 승인하면 (`design-approved.json` 잠금) M3는 **즉시** SpecDD 사이클을 자동 시작한다 (사용자 추가 입력 0). 이는 `scripts/dispatch-spec-cycle.sh`로 검증 가능하며, `chief-engineer-pm.md` §3.9에 명령형으로 기술. 이후 `idea-drift-detector.py` 훅이 spec 작성 단계에서 chosen_preview에 anchored 되어 있는지 강제.

사용자는 Gate H1, Gate H2 두 번만 개입합니다. 이외 모든 결정은 143-agent 조직이 자율 처리 (the 143-agent organization runs autonomously between the two human gates).

Expand Down
9 changes: 9 additions & 0 deletions plugins/preview-forge/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/auto-retro-trigger.py"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/post-h1-signal.py"
}
]
}
]
}
Expand Down
165 changes: 165 additions & 0 deletions plugins/preview-forge/hooks/post-h1-signal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""Preview Forge — Post-H1 sentinel writer (PostToolUse hook).

Watches Write tool calls. When `runs/<id>/design-approved.json` is
written AND `runs/<id>/chosen_preview.json.lock` already exists (i.e.
H1 has truly frozen — both lock artifacts present), this hook drops
`runs/<id>/.h1-frozen-signal` so M1 Run Supervisor's standup polling
loop can see "H1 done → kick SpecDD cycle" without M3 needing to be
re-prompted by the user.

Also appends a Blackboard SQLite row (matches `auto-retro-trigger.py`
convention — local repo standard) so the signal is observable in the
existing trace tooling.

Exit 0 always — advisory hook, never blocks the Write tool.

Hardening (W1.3 pattern):
- Bounded stdin read (4 MiB) with MemoryError catch, so a runaway
payload can't OOM the hook process.
"""
from __future__ import annotations

import json
import os
import re
import sqlite3
import sys
import time
from pathlib import Path

PLUGIN_ROOT = Path(os.environ.get("CLAUDE_PLUGIN_ROOT", ""))
CLAUDE_MD = PLUGIN_ROOT / "memory" / "CLAUDE.md"

# Same run_id charset as auto-retro-trigger.py — see that file's S-6
# comment for rationale (path traversal defense in depth).
_RUN_ID = r"r-[A-Za-z0-9][A-Za-z0-9_-]{0,63}"
DESIGN_APPROVED = re.compile(rf"runs/({_RUN_ID})/design-approved\.json$")

STDIN_CAP_BYTES = 4 * 1024 * 1024 # 4 MiB


def is_active() -> bool:
return CLAUDE_MD.exists()


def read_hook_input() -> dict:
try:
raw = sys.stdin.read(STDIN_CAP_BYTES + 1)
if len(raw) > STDIN_CAP_BYTES:
print(
"[preview-forge/post-h1-signal] warn: stdin exceeds 4MiB cap",
file=sys.stderr,
)
return {}
return json.loads(raw) if raw.strip() else {}
except (json.JSONDecodeError, MemoryError):
return {}


def write_sentinel(run_dir: Path) -> None:
sentinel = run_dir / ".h1-frozen-signal"
sentinel.write_text(
json.dumps(
{"ts": int(time.time()), "run_id": run_dir.name},
ensure_ascii=False,
)
+ "\n",
encoding="utf-8",
)


def append_blackboard(run_dir: Path) -> None:
bb_path = run_dir / "blackboard.db"
if not bb_path.parent.exists():
return
conn = sqlite3.connect(str(bb_path))
try:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS blackboard (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
agent_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT,
tier INTEGER,
dept TEXT
)
"""
)
conn.execute(
"""
INSERT INTO blackboard (agent_id, key, value, tier, dept)
VALUES (?, ?, ?, ?, ?)
""",
(
"post-h1-signal",
"h1.frozen",
json.dumps(
{"run_id": run_dir.name, "ts": int(time.time())},
ensure_ascii=False,
),
1,
"meta",
),
)
conn.commit()
finally:
conn.close()


def main() -> int:
if not is_active():
return 0

payload = read_hook_input()
tool = payload.get("tool_name") or payload.get("tool") or ""
if tool != "Write":
return 0
Comment on lines +117 to +119

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect H1 completion on Edit writes too

The new hook only handles Write, so if design-approved.json is updated through Edit (e.g., rerunning or correcting Gate H1 on an existing run file), no .h1-frozen-signal is emitted and the SpecDD auto-dispatch path is skipped. This should mirror the existing post-write hooks that handle multiple edit tools to avoid missing valid freeze events.

Useful? React with 👍 / 👎.


tool_input = payload.get("tool_input") or payload.get("input") or {}
path = tool_input.get("file_path") or tool_input.get("path") or ""
if not path:
return 0

m = DESIGN_APPROVED.search(path)
if not m:
return 0

run_id = m.group(1)
cwd = Path.cwd()
run_dir = cwd / "runs" / run_id

# Both lock artifacts must already be present — `design-approved.json`
# alone is not sufficient (it could be an in-progress draft write).
if not (run_dir / "chosen_preview.json.lock").exists():
return 0
if not run_dir.exists():
return 0

try:
write_sentinel(run_dir)
except Exception as e: # noqa: BLE001
print(
f"[preview-forge/post-h1-signal] sentinel warn: {e}",
file=sys.stderr,
)

try:
append_blackboard(run_dir)
except Exception as e: # noqa: BLE001
print(
f"[preview-forge/post-h1-signal] blackboard warn: {e}",
file=sys.stderr,
)

print(
f"[preview-forge/post-h1-signal] H1 frozen signal written for run={run_id}",
file=sys.stderr,
)
return 0


if __name__ == "__main__":
sys.exit(main())
69 changes: 69 additions & 0 deletions scripts/dispatch-spec-cycle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Preview Forge — Phase 1 H1→SpecDD auto-advance dispatch validator.
#
# After Gate H1 freezes (`chosen_preview.json.lock` + `design-approved.json`
# present + `idea.spec.json` from Socratic), M3 must immediately dispatch
# the SpecDD cycle (SPEC_LEAD → OpenAPI v1) without an extra user input.
# This script is the machine-verifiable side of that contract: it ONLY
# validates the run_dir's lock artifacts and emits a JSON line describing
# the dispatch the M3 caller must perform. The Task call itself stays in
# M3 markdown (chief-engineer-pm.md §3.9) — the script never invokes the
# LLM or shells out to claude.
#
# Contract (mirrors scripts/h1-modal-helper.sh style):
# - all 3 files exist and are non-empty
# → stdout: {"action":"dispatch","agent":"SPEC_LEAD",
# "input_dir":"<run_dir>","run_id":"<id>"}
# → exit: 0
# - any file missing or empty
# → stderr: "post-h1 dispatch precondition failed: <missing>"
# → exit: 2
# - bad arg
# → exit: 1
#
# Determinism: stdout is a single JSON line, no trailing whitespace —
# fixtures can byte-equal compare without `jq` round-tripping.

set -u

run_dir="${1:-}"
if [ -z "$run_dir" ]; then
echo "usage: dispatch-spec-cycle.sh <run_dir>" >&2
exit 1
fi

# Strip trailing slash for clean run_id derivation but keep a copy with
# trailing slash for input_dir (callers expect dir form).
run_dir_norm="${run_dir%/}"
run_id="$(basename "$run_dir_norm")"

required=(
"$run_dir_norm/chosen_preview.json.lock"
"$run_dir_norm/design-approved.json"
"$run_dir_norm/idea.spec.json"
)

for f in "${required[@]}"; do
if [ ! -s "$f" ]; then
echo "post-h1 dispatch precondition failed: $f" >&2
exit 2
fi
done

# Emit JSON via python3 to avoid shell-escaping bugs on paths with quotes.
# Fail-closed: if python3 is missing we MUST NOT emit a malformed payload.
python3 -c '
import json, sys
sys.stdout.write(json.dumps({
"action": "dispatch",
"agent": "SPEC_LEAD",
"input_dir": sys.argv[1],
"run_id": sys.argv[2],
}, ensure_ascii=False))
sys.stdout.write("\n")
' "$run_dir_norm/" "$run_id" || {
echo "dispatch-spec-cycle.sh: python3 unavailable — cannot encode JSON" >&2
exit 1
}

exit 0
Loading