대상 서비스, 메딕, 그리고 인터록
시스템은 세 축입니다. 대상 서비스는 CDK로 배포하는 작은 주문 API(HTTP API + Lambda + DynamoDB). 카오스는 3가지 장애를 주입하는 스크립트. 메딕은 EC2에서 도는 셸 + Claude 헤드리스로, 런북을 읽고 감별 진단해 aws CLI로 직접 치료합니다. 그리고 메딕의 손은 PreToolUse 인터록이 붙잡습니다, 파괴적 명령은 exit 2로 차단됩니다.
┌── chaos.sh (1 CONFIG_POISON / 2 THROTTLE / 3 ENV_BREAK / blind)
v
브라우저/curl ── HTTP API ── Lambda shop ── DynamoDB (flags + orders)
^ [대상 서비스, PatientStack]
│ /health 순찰, 처방 후 검증
[EC2] medic.sh 루프 ─┤
└ claude -p (헤드리스, RUNBOOK 감별 진단)
├ --allowed-tools: aws logs / dynamodb / lambda / curl / Read
├ PreToolUse 인터록: 파괴 명령 exit 2 차단
└ 결과 → INCIDENTS.md (판정 / ROOT_CAUSE / MTTR / 비용)
캡스톤 A(Ship It)를 먼저 했다면 M0의 상당 부분은 확인만으로 통과합니다. 90분 시점에 M3를 못 끝냈다면 부록 컷라인을 적용하세요.
발사 준비, 계획 승인까지
15분환경 점검 후 superpowers로 4-Phase 계획을 승인합니다. 캡스톤 A를 마친 계정이라면 bootstrap과 플러그인은 이미 준비된 상태입니다.
aws sts get-caller-identity --query Account --output text
node -v && jq --version && npx cdk --version
mkdir -p ~/capstone/self-heal && cd ~/capstone/self-heal
npx cdk init app --language typescript
(npx cdk bootstrap > /tmp/bootstrap.log 2>&1 &) # 이미 부트스트랩된 계정은 수 초 내 통과
git add -A && git commit -m "chore: cdk scaffold" 2>/dev/null || true
/plugin
목록에 superpowers가 없으면 /plugin install superpowers@claude-plugins-official로 설치하고
세션을 재시작하세요 (마켓플레이스 미등록 시 /plugin marketplace add anthropics/claude-plugins-official 선행).
/superpowers:brainstorming 다음 요구사항으로 "Self-Healing 운영실"을 설계하자.
1) 대상 서비스: CDK TypeScript 단일 스택(PatientStack), HTTP API + Lambda(Node 22) + DynamoDB 단일 테이블(pk/sk)
- GET /health: flags 항목(pk=flags, sk=flags)의 force_error가 true이거나 테이블 접근 실패면 500, 정상이면 200 {"ok":true}
- GET /order: 정상일 때 주문 항목을 기록하고 order_id 반환
- Outputs: ApiUrl, TableName, ShopFnName. 리전 ap-northeast-2, 리소스는 DESTROY
2) 운영 도구는 별도 셸 스크립트: chaos.sh(장애 주입 3종), medic.sh(헤드리스 진단과 복구), RUNBOOK.md(감별 진단표)
3) 안전장치: PreToolUse 인터록 훅으로 파괴적 aws 명령 차단, 메딕은 --allowed-tools로 손발 제한
4) 구현 순서 Phase 1(대상 서비스 스택) → Phase 2(런북+카오스) → Phase 3(메딕) → Phase 4(인터록+데몬+드릴 스킬)
5) 시간 제약 2시간: 단위 테스트 생략, 각 Phase는 smoke curl과 드릴 1회로 검증을 갈음한다
결정이 필요하면 이 범위 안에서 최소한으로만 물어봐.
auto mode on을 선언해 두면 superpowers가
brainstorming → write-plan → execute-plan의 단계 전환과 승인 대기를 자동으로 이어 갑니다.
시간이 빠듯할 때 유용합니다. 단계마다 직접 검토하려면 기본(수동)을 유지하고, 되돌리려면
auto mode off입니다.
치트시트: 인증 없음(데모), 알림 채널 불필요(INCIDENTS.md 파일로 충분),
데몬은 nohup 백그라운드면 충분(systemd는 참고).
브레인스토밍 후 /superpowers:write-plan으로 계획을 만들고 Phase 4개 구성을 확인해 승인하세요.
pwd로 확인한 그 경로에서 실행하세요.
대상 서비스 배포, 정상 상태의 기준선
25분Phase 1을 실행해 대상 서비스를 배포하고, 건강할 때의 모습(health 200, order 성공, 로그 흐름)을 눈에 익힙니다. 진단은 기준선을 아는 데서 시작합니다.
/superpowers:execute-plan 계획의 Phase 1(대상 서비스 스택)만 구현하고 멈춰줘. 구현 후 npx cdk synth --quiet 검증까지 하고 보고해.
npx cdk deploy --require-approval never --outputs-file cdk-outputs.json
API=$(jq -r '.PatientStack.ApiUrl' cdk-outputs.json)
FN=$(jq -r '.PatientStack.ShopFnName' cdk-outputs.json)
export OPS_TARGET_API="$API" OPS_OUTPUTS="$PWD/cdk-outputs.json"
curl -s "$API/health" | jq .
curl -s "$API/order" | jq .
aws logs tail "/aws/lambda/$FN" --since 5m | tail -5
{ "ok": true }
{ "ok": true, "order_id": "1753221049-a1b2c3" }
2026-07-22T22:30:11 START RequestId: ...
2026-07-22T22:30:11 END RequestId: ...
막힐 때 열어보기, 완성본 (대상 서비스 스택 3파일)
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigwv2 from "aws-cdk-lib/aws-apigatewayv2";
import * as integrations from "aws-cdk-lib/aws-apigatewayv2-integrations";
export class PatientStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const table = new dynamodb.Table(this, "ShopTable", {
partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
sortKey: { name: "sk", type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const shopFn = new lambda.Function(this, "ShopFn", {
runtime: lambda.Runtime.NODEJS_22_X,
handler: "shop.handler",
code: lambda.Code.fromAsset("lambda"),
environment: { TABLE_NAME: table.tableName },
timeout: cdk.Duration.seconds(10),
});
table.grantReadWriteData(shopFn);
const api = new apigwv2.HttpApi(this, "PatientApi");
const integ = new integrations.HttpLambdaIntegration("ShopInt", shopFn);
api.addRoutes({ path: "/health", methods: [apigwv2.HttpMethod.GET], integration: integ });
api.addRoutes({ path: "/order", methods: [apigwv2.HttpMethod.GET], integration: integ });
new cdk.CfnOutput(this, "ApiUrl", { value: api.apiEndpoint });
new cdk.CfnOutput(this, "TableName", { value: table.tableName });
new cdk.CfnOutput(this, "ShopFnName", { value: shopFn.functionName });
}
}
import * as cdk from "aws-cdk-lib";
import { PatientStack } from "./patient-stack";
const app = new cdk.App();
new PatientStack(app, "PatientStack", {
env: { region: process.env.CDK_DEFAULT_REGION ?? "ap-northeast-2" },
tags: { project: "capstone-self-heal" },
});
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand, PutCommand } from "@aws-sdk/lib-dynamodb";
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.TABLE_NAME;
async function readFlags() {
const res = await ddb.send(new GetCommand({
TableName: TABLE,
Key: { pk: "flags", sk: "flags" },
}));
return res.Item ?? {};
}
export const handler = async (event) => {
const path = event.rawPath ?? "/";
try {
const flags = await readFlags();
if (path.endsWith("/health")) {
if (flags.force_error) {
return { statusCode: 500, body: JSON.stringify({ ok: false }) };
}
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
}
if (path.endsWith("/order")) {
if (flags.force_error) {
return { statusCode: 500, body: JSON.stringify({ ok: false, error: "order rejected" }) };
}
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
await ddb.send(new PutCommand({
TableName: TABLE,
Item: { pk: "order", sk: id, at: new Date().toISOString() },
}));
return { statusCode: 200, body: JSON.stringify({ ok: true, order_id: id }) };
}
return { statusCode: 404, body: JSON.stringify({ ok: false, error: "not found" }) };
} catch (err) {
console.error("SHOP_ERROR", err.name, err.message);
return { statusCode: 500, body: JSON.stringify({ ok: false }) };
}
};
런북과 카오스, 병을 정의하다
20분서브에이전트가 런북을 쓰는 동안(Chapter 2), 여러분은 카오스 스크립트를 배치하고 장애 1종을 수동으로 주입과 처방해 봅니다. 메딕에게 시키기 전에 사람이 먼저 한 번 고쳐 봐야 합니다.
서브에이전트를 사용해 RUNBOOK.md를 작성해줘. 요구사항:
- 대상은 PatientStack이고 자원 이름은 cdk-outputs.json의 TableName, ShopFnName을 쓴다고 명시
- "감별 진단표" 표: 3가지 장애(ENV_BREAK, CONFIG_POISON, THROTTLE)의 증상 / 확인 명령 / 판정 / 원인 코드
핵심 신호: ENV_BREAK는 로그의 ResourceNotFoundException, CONFIG_POISON은 flags.force_error=true,
THROTTLE은 5xx인데 새 로그가 전혀 없고 get-function-concurrency가 0
- "처방" 절: 각 원인의 복구 aws 명령 한 줄씩 (ENV_BREAK 복구값은 cdk-outputs.json의 TableName)
- "검증" 절: /health 200 재확인, ENV 복구는 반영에 수 초 걸림 명시
cat > chaos.sh << 'CHAOSEOF'
#!/bin/bash
# 사용: ./chaos.sh 1|2|3|blind (1 CONFIG_POISON, 2 THROTTLE, 3 ENV_BREAK)
set -euo pipefail
OUTS="${OPS_OUTPUTS:-cdk-outputs.json}"
TABLE=$(jq -r '.PatientStack.TableName' "$OUTS")
FN=$(jq -r '.PatientStack.ShopFnName' "$OUTS")
MODE="${1:?Usage: ./chaos.sh 1|2|3|blind}"
if [ "$MODE" = "blind" ]; then
MODE=$(( (RANDOM % 3) + 1 ))
echo "[chaos] 블라인드 모드, 무엇이 주입됐는지는 메딕만 알게 됩니다"
fi
case "$MODE" in
1) aws dynamodb update-item --table-name "$TABLE" \
--key '{"pk":{"S":"flags"},"sk":{"S":"flags"}}' \
--update-expression 'SET force_error = :t' \
--expression-attribute-values '{":t":{"BOOL":true}}' ;;
2) aws lambda put-function-concurrency --function-name "$FN" \
--reserved-concurrent-executions 0 ;;
3) aws lambda update-function-configuration --function-name "$FN" \
--environment 'Variables={TABLE_NAME=broken-table-404}' ;;
*) echo "unknown mode: $MODE"; exit 1 ;;
esac
echo "[chaos] 주입 완료. 이제 메딕이 일하게 두세요."
CHAOSEOF
chmod +x chaos.sh
./chaos.sh 1
curl -s -o /dev/null -w 'health: %{http_code}\n' "$API/health" # 500이어야 정상적인 비정상
TABLE=$(jq -r '.PatientStack.TableName' cdk-outputs.json)
aws dynamodb update-item --table-name "$TABLE" \
--key '{"pk":{"S":"flags"},"sk":{"S":"flags"}}' \
--update-expression 'SET force_error = :f' \
--expression-attribute-values '{":f":{"BOOL":false}}'
curl -s -o /dev/null -w 'health: %{http_code}\n' "$API/health" # 다시 200
막힐 때 열어보기, 완성본 (RUNBOOK.md)
# Patient Service Runbook
대상: PatientStack (HTTP API + shop Lambda + DynamoDB)
자원 이름은 cdk-outputs.json의 TableName, ShopFnName을 사용한다.
## 감별 진단표
| 증상 | 확인 명령 | 판정 | 원인 코드 |
|---|---|---|---|
| /health 500, 로그에 SHOP_ERROR ResourceNotFoundException | aws logs tail /aws/lambda/<ShopFnName> --since 10m | Lambda 환경변수 TABLE_NAME 오염 | ENV_BREAK |
| /health 500, 로그는 정상 흐름, flags.force_error가 true | aws dynamodb get-item --table-name <TableName> --key '{"pk":{"S":"flags"},"sk":{"S":"flags"}}' | 설정 플래그 오염 | CONFIG_POISON |
| /health 5xx인데 새 로그가 전혀 안 생김 | aws lambda get-function-concurrency --function-name <ShopFnName> 이 0 | 예약 동시성 0으로 질식 | THROTTLE |
## 처방
- CONFIG_POISON: aws dynamodb update-item --table-name <TableName> --key '{"pk":{"S":"flags"},"sk":{"S":"flags"}}' --update-expression 'SET force_error = :f' --expression-attribute-values '{":f":{"BOOL":false}}'
- THROTTLE: aws lambda delete-function-concurrency --function-name <ShopFnName>
- ENV_BREAK: aws lambda update-function-configuration --function-name <ShopFnName> --environment 'Variables={TABLE_NAME=<TableName>}' (올바른 값은 cdk-outputs.json에서 읽는다)
## 검증
처방 후 curl -s <ApiUrl>/health 가 200 {"ok":true} 를 반환할 때까지 확인한다.
ENV_BREAK 처방은 함수 설정 반영에 수 초가 걸릴 수 있다.
메딕 에이전트, 진단과 치료의 자동화
30분Chapter 5의 헤드리스 문법이 전부 들어간 medic.sh를 배치하고, 장애를 주입한 뒤 메딕 혼자 고치는 것을 지켜봅니다.
cat > medic.sh << 'MEDICEOF'
#!/bin/bash
# medic.sh - 대상 서비스 점검, 비정상이면 Claude 헤드리스로 진단과 복구, 결과를 INCIDENTS.md에 기록
set -uo pipefail
API="${OPS_TARGET_API:?OPS_TARGET_API 필요 (대상 서비스 ApiUrl)}"
OUTS="${OPS_OUTPUTS:-cdk-outputs.json}"
MAX_WAIT="${MEDIC_MAX_WAIT:-60}"
code=$(curl -s -o /tmp/health.json -w '%{http_code}' -m 5 "$API/health" || echo 000)
if [ "$code" = "200" ]; then
echo "[medic] healthy"
exit 0
fi
T0=$(date +%s)
echo "[medic] UNHEALTHY (http $code), Claude 진단 시작"
REPORT=$(claude -p "$(cat <<PROMPT
당신은 SRE 메딕 에이전트입니다. 대상 서비스 API가 비정상입니다 (HTTP $code).
런북:
$(cat RUNBOOK.md)
스택 출력(cdk-outputs.json):
$(cat "$OUTS")
마지막 헬스 응답: $(cat /tmp/health.json 2>/dev/null)
런북의 감별 진단표 순서로 원인을 특정하고, 런북의 처방 명령으로 직접 복구한 뒤
curl로 /health 200을 확인하세요. 보고서 마지막 줄은 정확히 다음 형식 한 줄로 끝내세요:
ROOT_CAUSE: <CONFIG_POISON|THROTTLE|ENV_BREAK>
PROMPT
)" --output-format json --max-turns 15 --max-budget-usd 1.00 \
--allowed-tools "Bash(aws logs *)" "Bash(aws dynamodb *)" "Bash(aws lambda *)" "Bash(curl *)" "Read")
echo "$REPORT" | jq -r '.result' > /tmp/medic_report.md
COST=$(echo "$REPORT" | jq -r '.total_cost_usd // 0')
MTTR=""
i=0
while [ "$i" -lt "$((MAX_WAIT / 2))" ]; do
c2=$(curl -s -o /dev/null -w '%{http_code}' -m 5 "$API/health" || echo 000)
if [ "$c2" = "200" ]; then
MTTR=$(( $(date +%s) - T0 ))
break
fi
sleep 2
i=$((i + 1))
done
TS=$(date '+%F %T')
if [ -n "$MTTR" ]; then
CAUSE=$(grep -o 'ROOT_CAUSE: [A-Z_]*' /tmp/medic_report.md | tail -1)
{
echo "## $TS RECOVERED, MTTR ${MTTR}s, 비용 \$$COST"
echo "$CAUSE"
echo
cat /tmp/medic_report.md
echo
} >> INCIDENTS.md
echo "[medic] RECOVERED in ${MTTR}s / ${CAUSE:-원인 미표기}"
exit 0
else
{
echo "## $TS FAILED, ${MAX_WAIT}s 내 미회복, 비용 \$$COST"
cat /tmp/medic_report.md
echo
} >> INCIDENTS.md
echo "[medic] 복구 실패, 사람 호출이 필요합니다"
exit 1
fi
MEDICEOF
chmod +x medic.sh
bash -n medic.sh && echo "문법 OK"
--max-turns 15와 --max-budget-usd 1.00이 폭주와 지출을 막고,
--allowed-tools는 공백 구분 다중 인자로 aws logs / dynamodb / lambda / curl / Read만 허용합니다.
이 계정은 Admin이므로 실무 전환 시에는 메딕 전용 IAM 정책(로그 읽기 + 해당 테이블 UpdateItem + 해당 함수 설정 변경)으로
이중 방어하는 것이 Chapter 3의 정석입니다. Claude Code가 Bedrock 라우팅이면
ANTHROPIC_MODEL='global.anthropic.claude-sonnet-4-6' 한 줄로 메딕의 두뇌를 지정합니다.
./chaos.sh 1
./medic.sh
tail -8 INCIDENTS.md
[chaos] 주입 완료. 이제 메딕이 일하게 두세요.
[medic] UNHEALTHY (http 500), Claude 진단 시작
[medic] RECOVERED in 41s / ROOT_CAUSE: CONFIG_POISON
## 2026-07-22 22:41:05 RECOVERED, MTTR 41s, 비용 $0.18
ROOT_CAUSE: CONFIG_POISON
1) 헬스 500 확인, 로그는 정상 흐름 → 감별표 2행
2) flags.force_error=true 확인, 처방 적용
3) /health 200 재확인
이 장면이 핵심입니다. 여러분은 명령 두 줄만 쳤고, 원인 규명과 aws 처방은 전부 에이전트가 했습니다. THROTTLE(2번)로 한 번 더 돌려 감별이 달라지는지 확인해 보세요. 새 로그가 안 생기는 장애는 로그를 읽어도 안 보입니다, 그래서 런북이 get-function-concurrency를 가르쳐 둔 것입니다.
안전 인터록과 무인 순찰
20분자율 에이전트에게 가장 먼저 줄 것은 자유가 아니라 브레이크입니다. PreToolUse 인터록(Chapter 4)으로 파괴적 명령을 차단하고, 메딕을 30초 주기 무인 데몬으로 올립니다.
mkdir -p .claude/hooks
cat > .claude/hooks/ops-interlock.sh << 'LOCKEOF'
#!/bin/bash
# PreToolUse(Bash) 인터록: 메딕이 절대 쓰면 안 되는 파괴적 명령 차단
INPUT=$(cat)
if command -v jq >/dev/null 2>&1; then
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
else
CMD=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
fi
BLOCK='aws (iam |s3 rb|s3 rm|dynamodb delete-table|cloudformation delete-stack|ec2 terminate|lambda delete-function )'
if echo "$CMD" | grep -qE "$BLOCK"; then
echo "[interlock] 차단: 파괴적 명령은 메딕 권한 밖입니다: $CMD" >&2
exit 2
fi
exit 0
LOCKEOF
chmod +x .claude/hooks/ops-interlock.sh
.claude/settings.json에 PreToolUse 훅을 추가해줘. matcher는 "Bash", command는 "$CLAUDE_PROJECT_DIR/.claude/hooks/ops-interlock.sh" 하나야. 기존 설정은 유지해.
echo '{"tool_input":{"command":"aws iam delete-role --role-name demo"}}' \
| .claude/hooks/ops-interlock.sh; echo "exit: $?"
echo '{"tool_input":{"command":"aws lambda delete-function-concurrency --function-name x"}}' \
| .claude/hooks/ops-interlock.sh; echo "exit: $?"
[interlock] 차단: 파괴적 명령은 메딕 권한 밖입니다: aws iam delete-role --role-name demo
exit: 2
exit: 0
치료 명령인 delete-function-concurrency는 통과하고,
delete-function 은 막히는 것이 포인트입니다. 새 Claude 세션에서
"aws iam delete-role을 실행해봐"라고 시켜 보면 훅이 exit 2로 도구 호출 자체를 거부하는 것을 볼 수 있습니다.
cat > medic-daemon.sh << 'DAEMONEOF'
#!/bin/bash
# 30초 주기 무인 순찰. 종료: kill %1 또는 pkill -f medic-daemon
while true; do
./medic.sh
sleep "${MEDIC_INTERVAL:-30}"
done
DAEMONEOF
chmod +x medic-daemon.sh
nohup ./medic-daemon.sh > medic.log 2>&1 &
sleep 2 && tail -2 medic.log
pkill -f medic-daemon 입니다.
블라인드 카오스, 진짜 시험
20분이제 무슨 장애가 들어갈지 여러분도 모릅니다. /drill 스킬로 블라인드 장애를 주입하고, 데몬이 규명한 원인과 MTTR을 확인합니다. 세 가지 장애를 전부 잡을 때까지 이어 가는 3연속 블라인드 챌린지가 오늘의 피날레입니다.
mkdir -p .claude/skills/drill
cat > .claude/skills/drill/SKILL.md << 'DRILLEOF'
---
description: 카오스 드릴 실행. 장애를 주입하고 메딕의 진단과 복구를 관전하며 MTTR을 보고한다.
disable-model-invocation: true
argument-hint: "[blind|1|2|3]"
allowed-tools: Bash(./chaos.sh *) Bash(./medic.sh) Bash(curl *) Bash(tail *) Bash(sleep *) Read
---
카오스 드릴을 집도하세요. 모드: $ARGUMENTS (기본 blind)
1. `./chaos.sh $ARGUMENTS` 로 장애를 주입하세요.
2. `curl -s -o /dev/null -w '%{http_code}' $OPS_TARGET_API/health` 로 대상 서비스가 실제로 쓰러졌는지 확인하세요.
3. 데몬이 돌고 있지 않다면 `./medic.sh` 를 직접 1회 실행하세요.
4. INCIDENTS.md의 마지막 엔트리를 읽고 다음을 표로 보고하세요: 판정(RECOVERED/FAILED), ROOT_CAUSE, MTTR, 비용.
5. 마지막으로 /health 가 200인지 재확인하고 "드릴 종료"를 선언하세요.
주의: 직접 aws 명령으로 고치지 마세요. 치료는 메딕의 일입니다. 당신은 드릴 집도의입니다.
DRILLEOF
echo "새 Claude 세션에서 /drill 사용 가능"
/drill blind
| 판정 | ROOT_CAUSE | MTTR | 비용 |
| RECOVERED | THROTTLE | 52s | $0.14 |
드릴 종료. /health 200 재확인 완료.
grep -c "RECOVERED" INCIDENTS.md | xargs echo "오늘 자동 복구 횟수:"
grep -o 'ROOT_CAUSE: [A-Z_]*' INCIDENTS.md | sort | uniq -c # 원인별 격파 현황
git add -A && git commit -m "feat: self-healing ops capstone"
pkill -f medic-daemon # 데몬 종료
# 실습을 마쳤다면 비용 정리 (이어서 볼 계획이면 유지):
# npx cdk destroy --force
하네스 엔지니어링, 작품을 성숙시키고 등급을 받다
+15분 옵션지금까지 만든 것은 결과물입니다. 이 옵션은 그 결과물을 만든 작업 환경, 즉 하네스 (CLAUDE.md, hooks, skills, commands, agents)를 표준 구조로 성숙시키고(project-init), 그 품질을 6개 차원(정확성, 안전성, 완전성, 실행 가능성, 일관성, 검증 가능성)에서 채점받는(harness-eval) 단계입니다. superpowers의 3단(brainstorm, write-plan, execute-plan) 뒤에 4단 성숙화와 5단 평가를 붙이는 셈입니다.
claude plugin marketplace add https://github.com/whchoi98/project-init
claude plugin install project-init@project-init
claude plugin marketplace add https://github.com/whchoi98/harness-eval
claude plugin install harness-eval@harness-eval
claude plugin list
설치 후 새 Claude 세션을 시작하세요, superpowers 때와 같은 규칙입니다.
/project-init:init-project .
기존 프로젝트 감지 모드로 동작합니다. 이 캡스톤에서 손으로 만든 ops-interlock 훅과 /drill 스킬, RUNBOOK.md 같은 기존 하네스는 보존하고, 없는 것(문서 스캐폴딩, 시크릿 스캔 훅, 테스트 프레임워크, /review와 /test-all 커맨드 등)만 채웁니다. CLAUDE.md가 이미 있으면 덮어쓰기 전에 물어봅니다. 끝나면 이어서:
/project-init:sync-docs
## Sync Report
### Quality Scores (Before -> After)
| ./CLAUDE.md | C (62) | B (84) | +22 |
| ./lambda/CLAUDE.md | F (12) | C (58) | +46 |
### Changes Made
- Files created: 4 / Files updated: 2 / Runbooks missing: 1
/harness-eval:quick
{"mode": "quick", "scores": {"overall": 7.2, "grade": "B"}, "checklist": {"pass": 13, "warn": 2, "fail": 1}}
ops-interlock은 harness-eval의 안전성 차원이 가장 좋아하는 종류의 장치입니다. 자율 에이전트와 가드레일을 함께 배포한 프로젝트가 높은 등급을 받습니다. 여유가 있다면 이 캡스톤에서 실제로 내린 결정을 기록으로 남기세요:
/add-adr interlock-first-safety
/add-runbook medic-drill
컷라인과 트러블슈팅
비상용90분 시점 기준으로 순서대로 적용하세요. 어떤 컷에서도 M3의 메딕 1차 드릴과 M5의 블라인드는 지킵니다.
| 컷 | 조치 | 절약 |
|---|---|---|
| CUT 1 | M4 데몬 생략, 메딕은 수동 실행으로 진행 (인터록은 유지) | ~8분 |
| CUT 2 | M2 수동 드릴을 주입까지만 하고 처방은 런북 눈으로 확인 | ~5분 |
| CUT 3 | M5 블라인드를 모드 2(THROTTLE) 고정으로 대체 | ~5분 |
| 증상 | 원인 | 처방 |
|---|---|---|
| 메딕이 원인을 못 찾음 | RUNBOOK.md 경로 또는 cdk-outputs.json 미존재 | worktree에서 두 파일 존재 확인, OPS_OUTPUTS 경로 점검 |
| ENV_BREAK 복구 후에도 잠시 500 | 함수 설정 반영 지연 | 정상, medic의 검증 루프가 기다립니다 (최대 60초) |
| medic이 즉시 healthy로 끝남 | 카오스가 다른 스택/리전에 주입됨 | OPS_OUTPUTS가 현재 worktree의 outputs인지 확인 |
| claude가 돈/턴 한도로 중단 | --max-budget-usd 또는 --max-turns 도달 | INCIDENTS의 FAILED 기록 확인 후 상한 상향, 런북 신호를 더 구체화 |
| INCIDENTS.md가 안 생김 | OPS_TARGET_API 미설정 또는 medic을 다른 폴더에서 실행 | export 재확인, worktree에서 실행 |
| 인터록이 치료 명령까지 차단 | BLOCK 정규식 과잉 | ops-interlock.sh의 패턴 확인, delete-function 뒤 공백 유지 |
| 블라인드가 매번 같은 모드 | RANDOM 시드 아님, 우연 | 수 회 반복 또는 모드 지정 드릴로 3종 모두 검증 |
마무리, 챕터 소환 지도
| 캡스톤 장면 | 소환된 챕터 | 핵심 역량 |
|---|---|---|
| brainstorm → 4-Phase 계획 | Ch1 + superpowers | 계획 우선, 요구사항으로 워크플로 조종 |
| 런북 병렬 작성 | Ch2 | 서브에이전트 위임, 문서도 병렬로 |
| 메딕 도구 제한, IAM 이중화 원칙 | Ch3 | 최소 권한 사고, Bedrock 모델 지정 |
| PreToolUse 인터록, /drill 스킬 | Ch4 | 훅은 가드레일이다, exit 2의 힘 |
| medic.sh 전체 | Ch5 | 헤드리스 3중 잠금, JSON 파싱, exit 게이트, 정기 실행 |