| name | ghostwriter |
| description | Autonomous GitHub/GitLab bot. Monitors repos and creates PRs/MRs/comments under bot or user identity per platform. |
| metadata | {"openclaw":{"requires":{"bins":["jq","yq","git"],"env":[]}}} |
ghostwriter cycle skill
๋น์ ์ OpenClaw agent๋ก์ cron์ด fire๋ ๋๋ง๋ค ์ด skill์ ๋ฐ์์ ํ ํ์ฐจ๋ฅผ ๋๋ค. ํ์ฐจ์ ๊ณจ๊ฒฉ์ deterministic ์คํฌ๋ฆฝํธ๊ฐ ์ก๊ณ , ๋ณธ๋ฌธ/ํจ์น/issue ๋ด์ฉ ์์ฑ ๊ฐ์ด LLM ํ๋จ์ด ํ์ํ ๋ถ๋ถ๋ง ๋น์ ์ด ํ๋ค.
1. ํ์ฐจ ์ง์
โ lease๋ฅผ ๋จผ์ ์ก๊ณ , ๋ชจ๋ ์์
์ด ๋๋ ๋ค ํ์ด์ผ ํ๋ค
ํ์ฐจ์ ๋ชจ๋ ์์
์ ๋จ์ผ lease ์์์ ์ผ์ด๋์ผ ํ๋ค. cycle.sh ์์ฒด๋ lease๋ฅผ ์ก์ง ์๋๋ค. ๊ทธ ์ฑ
์์ ์ด SKILL.md ๊ฐ ์ง์ด์ง๋ค โ agent๊ฐ ์ง์ acquire-lease / release-lease ๋ฅผ ํธ์ถํด cycle.sh + per-event ๋ณธ๋ฌธ ์์ฑ + post.sh + scan.sh ๊น์ง ๋ชจ๋ ๊ฐ์ lock ์์ ๋ฌ์ผ ํ๋ค.
RUN_ID="$(date -u +%FT%TZ)-$$"
TTL=7200
if ! bash {baseDir}/scripts/state.sh acquire-lease "$RUN_ID" "$TTL"; then
exit 0
fi
trap 'bash {baseDir}/scripts/state.sh release-lease "$RUN_ID"' EXIT
์ด์ bash {baseDir}/scripts/cycle.sh ๋ฅผ ํธ์ถํ๋ค. ์ถ๋ ฅ์ NDJSON ๋ผ์ธ:
{"kind":"event","event":{...}} โ ์ฒ๋ฆฌํ ์ด๋ฒคํธ 1๊ฑด (์ด๋ฏธ inFlightEvents ์ claim ๋จ)
{"kind":"signal","name":"idle-scan"} โ ์ด๋ฒคํธ 0๊ฑด + idle scan due + polling ์ฑ๊ณต
{"kind":"signal","name":"poll-failed"} โ polling ์์ฒด๊ฐ ์คํจ. ์ด ํ์ฐจ๋ idle-scan ๋ ์ ๋๋ค. ๊ทธ๋ฅ done ๊น์ง ๊ธฐ๋ค๋ฆฌ๊ณ ์ข
๋ฃ
{"kind":"signal","name":"paused"} โ state.json.paused == true
{"kind":"done"} โ cycle.sh ๊ฐ ํ ์ผ์ ๋
paused / poll-failed ๋ฅผ ๋ฐ์ผ๋ฉด ํธ๋ฉ์ด lease ๋ฅผ ํ์ด์ฃผ๊ณ ์ฆ์ ํ์ฐจ ์ข
๋ฃ. event ๋ฅผ ๋ฐ์ ๋๋ง๋ค ์๋ ยง2 ๋ฅผ ํ ๋ฒ์ฉ ์ํ. idle-scan ์ ๋ฐ์ผ๋ฉด ยง3.
์คํฌ๋ฆฝํธ๋ค์ ์ ๋ ๊ฒฝ๋ก๋ฅผ ์ง์ ์ ๋ฐ๋๋ค. ๋ชจ๋ ๊ฒฝ๋ก๋ config.yaml ์ paths ํ๋์์ ์ฝ๋๋ค. ${OPENCLAW_WORKSPACE} ๊ฐ์ ์
ธ ๋ณ์๋ ๋ณธ๋ฌธ ์์์ ์ง์ ์ฐธ์กฐํ์ง ๋ง ๊ฒ.
Long cycles: idle scan ์ make test ๊ฐ ๊ธธ์ด lease TTL ๊ฐ๊น์ด ๋ค๊ฐ๊ฐ๋ฉด bash {baseDir}/scripts/state.sh renew-lease "$RUN_ID" "$TTL" ๋ก ์ฐ์ฅ ๊ฐ๋ฅ.
2. ์ด๋ฒคํธ ์ฒ๋ฆฌ
์ด๋ฒคํธ JSON์ ๋ค์ ํ๋๋ฅผ ๊ฐ๋๋ค:
provider: github | gitlab
repo: owner/repo (GitHub) ๋๋ projectId (GitLab)
type: issue | pr | mr | icomment | rcomment | review | inote | mnote | reopen | trigger
id: stable event id (ack ์ฉ)
parentId: ๋๊ธ ํ๋ ์ถ์ ์ฉ ๋ถ๋ชจ issue/PR/MR id
targetType: ์ฝ๋ฉํธ๊ฐ ๋ค์ด๊ฐ ๋์ (issue | pr | mr)
targetNumber: ๊ทธ ๋์์ ๋ฒํธ
author, url, title, body, ts
ํ๋จ ํ๋ฆ:
- ์ด ์ด๋ฒคํธ์ ๋ต๋ณ์ด ํ์ํ๊ฐ? โ poll.sh ์ keep_event๊ฐ ์ด๋ฏธ ๊ฐํ ๊ฒ์ดํ
์ ํด์ stdout์ ๋๋ฌํ๋ค. ํต๊ณผํ ์ด์ ๋ ์
์ค ํ๋๋ค:
- ๋ณธ๋ฌธ์ commandMarker (
[ghostwriter]) ๋๋ (bot mode๋ฉด) @<๋ด๊ณ์ > mention ์ด ์์
- parentId ๊ฐ
stats.botEngagedIssues ์ ์์ (๋ด์ด ์ด์ ํ์ฐจ์ ๊ทธ issue/PR/MR ์ ์ฝ๋ฉํธ/์์ฑ์ผ๋ก engage ํ๊ณ , ์ด๋ฒ ํ์ฐจ์ ์ ์ก์
์ด ๋ถ์์)
- GitHub/GitLab assign/mention ์ด active bot/user identity ๋ฅผ ๋์์ผ๋ก ํด์
trigger ์ด๋ฒคํธ๋ก ๋ณํ๋จ
- ๋ต๋ณ ๋ณธ๋ฌธ(๋๋ PR description, issue ๋ณธ๋ฌธ)์ ์์ฑํ๋ค. ๋ณธ๋ฌธ ์ฒซ ์ค์
[bot:ghostwriter] ๊ฐ ๋๋ฝ๋ผ๋ post.sh๊ฐ ์๋ ๋ถ์ฐฉํ์ง๋ง, ์ฒ์๋ถํฐ prefix ํฌํจํด์ ์ฐ๋ ํธ์ด ๊ฐ๋
์ฑ์ ์ข๋ค
- ๋ณธ๋ฌธ์ ์์ ํ์ผ์ ์ ์ฅํ๊ณ post.sh ํธ์ถ:
event_file=$(mktemp); printf '%s' "$EVENT_JSON" >"$event_file"
body_file=$(mktemp); printf '%s' "$BODY" >"$body_file"
bash {baseDir}/scripts/post.sh comment "$event_file" "$body_file"
rm -f "$event_file" "$body_file"
post.sh ๊ฐ ์๋์ผ๋ก:
- prefix ๊ฒ์ฆ / ๋ถ์ฐฉ
- rolling 24h ํ๋ ์ฒดํฌ (์ด๊ณผ ์ silent skip + ack-processed)
- gh / glab API ํธ์ถ
- state ack-processed
- mark-engaged (๋ค์ ํ์ฐจ keep_event ๊ฐ follow-through ํ๋๋ก)
- user mode ๋ฉด SMTP ์ง์ ๋ฐ์ก (curl ๋ก
notifications.smtpUrl ์ฌ์ฉ)
post.sh ๊ฐ 0 ์ด ์๋ exit code ๋ฅผ ๋ด๋ฉด push-pending ์ด ์ด๋ฏธ ํธ์ถ๋์ด ๋ค์ ํ์ฐจ์ ์ฌ์๋๋๋ค. ๊ฐ์ ์ด๋ฒคํธ๋ฅผ ๊ฐ์ ํ์ฐจ์ ๋ ์๋ํ์ง ๋ง ๊ฒ.
autonomous flag: ์ฌ์ฉ์ ์์ฒญ์ ๋ตํ๋ PR / issue ์์ฑ์ post.sh pr ... / post.sh issue ... ๋ฅผ ์ธ์ ๊ทธ๋๋ก (๋ง์ง๋ง autonomous ์ธ์ ์๋ต = false) ํธ์ถํ๋ค. ๊ทธ๋ฌ๋ฉด idle scan ์ autonomousPrPerDay / autonomousIssuePerNDays ํ๋์ ๋ฌด๊ดํ๊ฒ ์งํ๋๋ค. autonomous ํ๋๋ idle scan ํญ์ฃผ ๋ฐฉ์ง์ฉ์ด๊ณ , ์ฌ์ฉ์๊ฐ ์ง์ ๋ถ๋ฅธ ์์
๊น์ง ํ๋๋ก silent skip ํ๋ฉด ์ฌ์ฉ์ ์ ๋ขฐ๊ฐ ๊นจ์ง๋ค.
trigger ์ด๋ฒคํธ (GitHub/GitLab assigned/mentioned)
์๋ต ์ฝ๋ฉํธ๋ง ๋ค๋ ๊ฒ ์๋๋ผ ์์
์ฐฉ์๊ฐ ์๋๋ค. ๋ค์ ํ๋ฆ์ ๊ถ์ฅ:
- ํด๋น issue/MR์ ์ฝ๊ณ ๋ฌด์์ ํด์ผ ํ ์ง ๋ณธ๋ฌธ์ ์ ๋ฆฌ
- ์ฝ๋ ์์
์ด ํ์ํ๋ฉด
bot/<topic> branch์์ ์งํ (scan.sh apply patch ํ๋ฆ๊ณผ ๋์ผํ๊ฒ ๊ตฌ์ฑ)
- ๊ฒฐ๊ณผ๋ฅผ ์ฝ๋ฉํธ๋ก ๋ณด๊ณ (post.sh comment)
reopen ์ด๋ฒคํธ
reopen๋ issue/PR์ ๋ค์ ์ดํด๋ณด๊ณ ๋ฌด์์ด ๋ฐ๋์๋์ง ๋ณธ๋ฌธ์ ์ ๋ฆฌํ ์ฝ๋ฉํธ๋ก ์๋ตํ๋ค.
rcomment (PR review line comment)
๋ณธ plan์ v1์์๋ line-level reply API ๋์ ์ผ๋ฐ PR ์ฝ๋ฉํธ๋ก ์๋ตํ๋ค. ๋ณธ๋ฌธ์์ ์ด๋ ๋ผ์ธ/ํ์ผ์ ๋ตํ๋์ง ๋ช
์ํ๋ค (์: re: src/foo.go:42 โ ...).
3. idle scan
{"kind":"signal","name":"idle-scan"} ๊ฐ ๋จ์ด์ง๋ฉด:
bash {baseDir}/scripts/scan.sh prepare
prepare๋ deterministic ํ๋ณด 1๊ฐ์ ์ปจํ
์คํธ JSON์ stdout์ผ๋ก ์ค๋ค. ํ์:
{
"provider": "github",
"repo": "guny524/kream_crawl",
"checkout": "/.../repos/github/guny524__kream_crawl",
"display": "kream_crawl",
"capabilities": { "pr": true, "issue": true },
"candidate": {
"source": "todoMarkers",
"score": 30,
"file": "scripts/run.sh",
"line": 42,
"summary": "...",
"evidence": "..."
}
}
ํ๋ณด๋ฅผ ๋ณด๊ณ ๋ ๊ฐ๋ ๊ฒฐ์ :
3-a. ํจ์น ๊ฐ๋ฅ
์์ fix๋ฉด patch payload๋ฅผ ์์ฑํด scan.sh apply patch ๋ก ๋๊ธด๋ค.
payload=$(mktemp)
cat >"$payload" <<JSON
{
"title": "[bot:ghostwriter] resolve TODO in scripts/run.sh",
"body": "...",
"branch": "bot/todo-run-sh",
"diff": "diff --git a/scripts/run.sh b/scripts/run.sh\n..."
}
JSON
bash {baseDir}/scripts/scan.sh apply patch "$payload"
rm -f "$payload"
scan.sh apply patch:
- patch ์ ์ฉ +
make test ์คํ (์์ผ๋ฉด)
- ํต๊ณผ ์ commit + push + PR ์์ฑ (post.sh pr)
- ์คํจ ๋๋ PR ํ๋ hit โ issue๋ก ์๋ ๊ฐ๋ฑ
3-b. ํจ์น ๋ถ๊ฐ
์กฐ์ฌ ํ์/๋ฆฌ์คํฌ ํผ/์ค์ฝํ ํผ์ด๋ฉด issue payload๋ง ๋ง๋ค์ด์ apply issue:
payload=$(mktemp)
cat >"$payload" <<JSON
{
"title": "[bot:ghostwriter] investigate possible race in cycle.sh",
"body": "..."
}
JSON
bash {baseDir}/scripts/scan.sh apply issue "$payload"
rm -f "$payload"
allowIssueCreation: false ๋๋ issue ํ๋ hit์ด๋ฉด silent skip (exit 64). ๊ทธ๋๋ ํ์ฐจ์ lastIdleScanAt ์ ๊ฐฑ์ ๋๋ค.
4. ํ์ฐจ ์ข
๋ฃ
{"kind":"done"} ๋ผ์ธ + ๋ชจ๋ ์ด๋ฒคํธ ์ฒ๋ฆฌ ์๋ฃ + (ํ์ ์) scan.sh apply ๊น์ง ๋๋ฌ์ผ๋ฉด ํ์ฐจ ์ข
๋ฃ. bash exit ์ EXIT trap ์ด state.sh release-lease "$RUN_ID" ๋ฅผ ํธ์ถํด lease ๋ฅผ ํ์ด์ค๋ค.
agent ๊ฐ ๋น์ ์ ์ข
๋ฃํด trap ์ด ์ ๋๋ฉด lease ๋ expiresAt ๊น์ง (default 2์๊ฐ) ๊ทธ๋๋ก ์ ์ง๋๊ณ , ๊ทธ ์ดํ cron ๋ถํฐ takeover ๋๋ค. ๊ทธ ์ฌ์ด ๋์ ์ cron ๋ค์ acquire-lease ์์ ๋งํ ์กฐ์ฉํ ์ข
๋ฃ๋๋ค.
5. ์ ๋ ํ์ง ๋ง ๊ฒ
gh auth setup-git / git config --global ์คํ
git push --force ๋๋ bot/ ์ธ branch์ push
- ํ ํฐ์ URL/log์ ๋
ธ์ถ (askpass.sh ์ธ ๊ฒฝ๋ก ๊ธ์ง)
- merge / close ์ด๋ฒคํธ์ ์๋ต
[bot:ghostwriter] prefix ์์ด ๋ณธ๋ฌธ ์ง์ API ํธ์ถ (post.sh ๋ง ์ฌ์ฉ)
- ๊ฐ์ ์ด๋ฒคํธ๋ฅผ ๊ฐ์ ํ์ฐจ์ ๋ ๋ฒ ์ฒ๋ฆฌ ์๋
- ํ์ฐจ ์์์ cycle.sh๋ฅผ ๋ ๋ฒ ํธ์ถ