AGENTS.md · n8n 모델·Prompt·Workflow 버전 관리 작업 규칙 (74일차 실습 폴더)
이 파일은 CLI 코딩 에이전트(Codex, Claude Code, Antigravity 등)가 이 폴더에서 n8n 워크플로우를 만들 때 따르는 규칙입니다. 사람도 읽을 수 있게 썼습니다. 73일차 규칙과 같고, 3절이 오늘 만드는 버전이 붙은 헬프데스크 · 릴리스 점검의 계약, 5절에 릴리스 정보 · 지시문 저장소 · 기본 지시문 · 응답 코드로 가르기 · 카나리 · 점검 기록 노드가 더해졌습니다.
1. 작업 순서 (항상 이 순서)
- 요청을 읽고, 만들 워크플로우의 입력 · 노드 순서 · 결과 모양을 3~5줄로 먼저 정리해 보여 준다. 버전 요청이면 무엇이 워크플로우 안에 남고, 무엇이 밖(데이터 API)으로 나가는지도 함께 보여 준다.
- 워크플로우 JSON을
my-work/<이름>.json에 쓴다. 이미 있으면 그 파일을 고친다 (새 파일을 만들지 않는다).
uv run tools/flow.py check my-work/<이름>.json → 오류(✘)가 없을 때까지 고친다.
- 끝나면 한국어로 짧게 보고한다: 바꾼 노드, check 결과, 그리고 사람이 실행할 명령 두 줄.
- push와 test는 사람이 실행해 결과를 확인한다. 사람이 "네가 push·test까지 해"라고 하면 직접 실행해도 된다 (push는 약 10초, n8n이 잠깐 재시작됨).
- 사람이 실패 보고서(
runs/*.json)를 알려 주면 실패한 케이스의 요청·응답을 읽고 원인을 설명한 뒤 고친다. 같은 곳을 3번 고쳐도 실패하면 멈추고 다른 방법을 제안한다.
- 문서(
my-work/*.md)를 써 달라는 요청이면 워크플로우를 만들지 않고 문서만 쓴다.
2. 환경 (바꾸지 말 것)
| 항목 |
값 |
| n8n |
2.42.6, 65일차 공식 설치 ~/day65-n8n (Docker 프로젝트 day65-n8n, 컨테이너 보통 day65-n8n-n8n-1), 브라우저 http://localhost:5678 |
| n8n 켜고 끄기 |
~/aio-01d4-stu/Last3Mon/65/n8n에서 docker compose start/stop (사람이 함, Ollama 컨테이너는 없음) |
| 게시된 웹훅 주소 |
밖에서 http://localhost:5678/webhook/<path>, n8n 안에서도 http://localhost:5678/webhook/<path> (같은 컨테이너) |
| OpenAI Credential |
id openAiCourse0001, name OpenAI (.env). 모델 이름: gpt-4.1-mini, gpt-5.4-nano (같은 Credential) |
| 다른 Credential |
쓰지 않음 (Ollama 등 없음) |
| 데이터 API |
n8n 안에서 http://data-api:8800, 터미널에서 http://localhost:8800 (74일차 판 Last3Mon/74/data-api: 73일차 + 지시문 저장소 + 릴리스 + 점검 기록 · 배포 정책) |
| 비교용 파일 |
reference/helpdesk-v1.json (rel-v1), reference/helpdesk-v2.json (rel-v2), 고치지 않는다 |
데이터 API (74일차 판)
| 요청 |
응답 |
GET /prompts/helpdesk |
{"name", "versions": ["p1", "p2", "p3"]} |
GET /prompts/helpdesk/<버전> |
{"name", "version", "text"} (지시문 글자) · 없으면 404 {"error": "prompt not found"} · 고장 스위치 prompts가 error면 500, slow면 8초 늦게 |
GET /releases/helpdesk |
{"name", "active": "1.0.0", "canary": null | {"release", "percent"}, "require_gate": false, "releases": {"1.0.0": {"prompt_version", "model", "note"}, ...}} |
POST /releases/helpdesk |
{"active": "1.1.0"} · {"canary": {"release": "1.1.0", "percent": 50}} · {"canary": null} (사람이 flow.py release로). 배포 정책이 켜져 있으면 마지막 점검 기록이 promote인 릴리스만, 아니면 409 {"error": "not gated", "release", "last_gate"} |
POST /releases/helpdesk/policy |
{"require_gate": true | false} (사람이 flow.py release helpdesk --require-gate on) |
POST /releases/helpdesk/reset |
처음 상태로 (active 1.0.0, 카나리 없음, 정책 꺼짐. 점검 기록은 남음) |
POST /gates/helpdesk |
점검 기록 한 줄 {"release", "verdict": "promote" | "rollback", "passed", "total", "source", ...} → {"stored": true, "count"} (Lab 03 과제 2, flow.py gate도 남김) |
GET /gates/helpdesk · POST /gates/helpdesk/clear |
{"name", "gates": [{"t", ...}]} · 비우기 {"cleared"} |
POST /admin/fault |
{"prompts": "ok" | "slow" | "error"} (faq · tickets도, 72일차와 같음) · flow.py fault prompts error |
GET /faq?q= · GET /tickets/<id>?requester= · /audit |
72 · 73일차와 같음 |
| 릴리스 |
지시문 |
모델 |
내용 |
| 1.0.0 |
p1 |
gpt-4.1-mini |
첫 릴리스 |
| 1.1.0 |
p2 |
gpt-4.1-mini |
FAQ 답 끝에 출처 번호 [FAQ-003] |
| 1.2.0 |
p3 |
gpt-4.1-mini |
답을 한 문장으로 짧게 |
| 2.0.0 |
p2 |
gpt-5.4-nano |
더 작은 모델 (모델만 바뀜) |
| 2.1.0 |
p9 |
gpt-4.1-mini |
저장소에 없는 지시문을 가리킴 (Lab 02 과제 2의 망가진 릴리스) |
3. 오늘 만드는 것의 계약 (요청에 다른 말이 없으면 이대로)
| 무엇 |
파일 · 웹훅 |
입력 (본문) |
결과 |
| 헬프데스크 |
my-work/helpdesk.json · POST /webhook/my-helpdesk (처음은 reference/helpdesk-v1.json을 복사) |
question(필수), user, release(있으면 그 릴리스로 고정 — 후보 시험용) |
{"route", "answer", "sources", "version": {"release", "prompt_version", "model", "channel", "prompt_source"}} |
| 릴리스 점검 |
my-work/release-check.json · POST /webhook/my-release-check |
release(필수) |
{"release", "passed", "total", "failed", "seconds", "verdict": "promote" | "rollback"} |
version.channel: stable(active) · canary(카나리 대상) · pinned(본문 release로 고정). version.prompt_source: 지시문을 저장소에서 가져왔으면 registry, 저장소가 멈춰 기본 지시문으로 답했으면 fallback.
- 없는 릴리스를 고정하면 HTTP 400
{"error": "unknown release", "release", "known": [...]}.
- 저장소가 멈추면(500 · 2초 넘음) 기본 지시문으로 답한다 (Lab 01 과제 2). 기본 지시문은 p1과 같은 글을 워크플로우 안에 둔다.
- 저장소가 404면(릴리스가 없는 지시문을 가리킴) 기본 지시문으로 넘어가지 않고 HTTP 400
{"error": "release broken", "release", "prompt_version"} (Lab 02 과제 2).
- 릴리스 점검(Lab 03 과제 2): 판단 뒤
POST http://data-api:8800/gates/helpdesk에 기록하고 결과에 recorded: true|false. 워크플로우가 배포(/releases에 POST)하지 않는다.
- 카나리 대상 규칙:
user 이름 글자 코드의 합 % 100 이 percent보다 작으면 카나리. 같은 사람은 늘 같은 쪽. (flow.py release helpdesk가 대상 직원을 보여 줌)
- 고정(
release) > 카나리 > active 순서로 정한다.
4. 워크플로우 파일 규칙
- 파일 하나에 워크플로우 하나. 노드
name은 영어로, 겹치지 않게. 노드마다 type, typeVersion, position, parameters.
- API 키를 JSON에 쓰지 않는다. LLM 노드는 위 Credential id만 쓴다.
- 웹훅은
POST, responseMode: "responseNode", 결과는 Respond to Webhook 노드로.
- 요청 본문은
$json.body.question처럼 body 아래. 다른 노드 뒤에서는 $('Question webhook').first().json.body.
5. 검증된 노드 (n8n 2.42.6에서 실행 확인)
릴리스 정보 (Set) · 모델 이름을 표현식으로
지시문 저장소 (HTTP) → Agent의 systemMessage
지시문 저장소가 멈추면 (Lab 01 과제 2)
Get prompt에 "options": {"timeout": 2000}, 노드에 "onError": "continueErrorOutput". main은 [ [성공 쪽], [실패 쪽] ].
- 성공 쪽
Registry prompt(Set, text = ={{ $json.text }}), 실패 쪽 Fallback prompt(Set, text = p1과 같은 글). 둘 다 Search FAQ로.
- 읽을 때:
"systemMessage": "={{ $('Registry prompt').isExecuted ? $('Registry prompt').first().json.text : $('Fallback prompt').first().json.text }}".
응답 코드로 가르기 (Lab 02 과제 2)
fullResponse면 다음 노드가 $json.statusCode · $json.body를 받는다. neverError면 404 · 500도 성공 쪽으로 온다 (시간 초과만 실패 쪽).
- 다음 Code에서
statusCode가 200 · 404 · 그 밖으로 가르고, If(문자열 equals)로 400 갈래.
릴리스 고르기 (HTTP → Code → If)
- 이름 글자 코드의 합:
[...name].reduce((sum, ch) => sum + ch.codePointAt(0), 0) % 100.
HTTP로 내 웹훅 부르기 (릴리스 점검) — 항목마다 한 번
- 앞 Code가 항목 6개를 돌려주면 HTTP 노드가 항목마다 한 번씩 부른다. 다음 Code에서
$input.all()과 $('Golden questions').all()을 같은 순서로 맞춰 읽는다.
점검 기록 (Lab 03 과제 2)
- 기록이 실패해도 판단은 보낸다 (
continueRegularOutput). 다음 Respond에서 $('Judge').first().json에 recorded: $json.stored === true를 더한다.
그 밖 (72 · 73일차와 같음)
- Webhook · If · Respond to Webhook · Search FAQ(HTTP
/faq) · Get ticket(HTTP /tickets, neverError) · Code.
- 오류 응답:
"responseBody": "={{ JSON.stringify({ error: 'unknown release', release: $json.release, known: $json.known }) }}", "options": {"responseCode": 400}.
6. 연결 (connections)
- 키는 보내는 노드 이름, 안의
node는 받는 노드 이름. If의 main은 [ [true 쪽], [false 쪽] ].
7. 표현식 주의
- 표현식은
=로 시작한다. {{ }} 안에서 객체를 만들 때 }}가 붙지 않게 띄어 쓴다.
- 모델 이름 · 지시문을 표현식으로 읽을 때 앞 노드 이름이 정확해야 한다 (
$('Release info'), $('Pick release')).
8. 테스트 파일 · 게이트
uv run tools/flow.py test <파일> -v --set release=1.1.0: 모든 요청 본문에 release를 더한다 (후보 시험). -v는 케이스마다 요청 · 응답 JSON을 보여 준다. 모든 명령이 -v를 받는다 (73일차와 같음).
uv run tools/flow.py gate <파일> --set release=1.1.0 --min-pass 1.0 --max-p95 3: 통과율과 p95로 PASS/FAIL (종료 코드 0/1). 결과를 /gates/helpdesk에 남긴다 (--no-record면 남기지 않음).
before · after 단계에 "status": 409를 적으면 그 응답 코드를 기대한다 (예: 정책이 배포를 거절하는지).
--no-before: 파일 맨 위 before를 건너뛴다 (Lab 03 예제 3, 카나리를 끈 뒤 다시 잴 때).
uv run tools/flow.py diff a.json b.json [--full]: 두 워크플로우의 노드 · 설정 · 연결 차이.
- 테스트를 통과시키려고 테스트 파일의 기대 결과를 바꾸지 않는다. 기대 결과가 틀렸다고 생각되면 사람에게 묻는다.
9. 하지 말 것
tools/, data-api/, requests/, reference/, 이 파일(AGENTS.md)을 고치지 않는다 (요청받은 경우만). 지시문 파일(data-api/data/prompts/)과 releases.json은 사람이 정한다.
- 워크플로우 안에서
/releases/...에 POST 하지 않는다 (배포 · 되돌리기는 사람이 flow.py release로).
- n8n 컨테이너를 지우거나
docker compose down을 하지 않는다. ~/day65-n8n(n8n 설치·비밀값)을 읽거나 고치지 않는다. Credential을 새로 만들지 않는다.
.env나 API 키를 읽어서 출력하지 않는다.