Drive a jinwon-int/nclex content PR through the full narrow-gate A2A pipeline — terminology lane dispatch, result-preservation rerun on verdict fail, coverage review projection, CI recovery (stale base.sha), content 2-lane dispatch with keyring-trusted worker routing, signed receipt posting, and a2a/receipts verification. Includes the sanctioned task watcher (watch-task.sh, #1389) for polling broker tasks with the correct terminal vocabulary. Use when authoring or shepherding an NCLEX 출제 PR (new_case/rewrite/remediation), when a terminology or content lane must be dispatched, when a lane returns review_verdict_failed, when polling an A2A lane task, or when receipts must be projected to the PR. 확립 2026-08-13 RRP-003(#249)·PA-006(#251) 2회 완주.
Resources
1Install
npx skillscat add jinwon-int/ccc-node/nclex-a2a-content-pipeline Install via the SkillsCat registry.
nclex A2A content pipeline (좁은 문 규율)
콘텐츠 PR은 A2A exact-head PASS 전 머지 금지. 전체 흐름:
출제 PR → ① terminology 레인 → 검토 기록 커밋 → ② CI green(자동 요청서) →
③ content 2-lane → ④ receipt 게시 → a2a/receipts green → 비작성자 승인·머지.
머지·A2A 디스패치·크로스계정 승인은 각각 사용자 승인을 받는다.
0. 전제
- 로컬 클론(예:
/root/work/nclex), 디스패치 작업장/root/work/nclex-dispatch/
(기존 manifest들이 템플릿 —manifest-pr249-*.json,manifest-pr251-*.json). - 디스패치 CLI:
a2a-nexus체크아웃의scripts/a2a-dispatch-round.mjs
(--manifest F --dry-run→--verify). edge secret은 브로커가 있는 노드의
로컬 env에서만 읽는다 — T1(로컬 브로커)=브로커 노드 env의BROKER_EDGE_SECRET(로컬 터널http://127.0.0.1:<port>), T2(원격 브로커)=브로커
호스트 a2a-nexus 체크아웃packages/broker/.env의EDGE_SECRET(브로커
호스트에서 SSH로 실행, 값 이동·출력 금지). - 신뢰 워커는 keyring
refs/a2a-public-keyring.json등록 노드만 — 레인별 등록
목록은 키링 파일이 정본이고 저자 노드는 구조적 제척. 미등록 노드의 PASS는a2a/receipts에서 무효 — 디스패치 전 등록 여부를 확인한다. - PR 본문에 기계 판독 8필드 필수(값=머지된 claim 레코드와 정확 일치):
- TASK_ID/TASK_KIND/AUTHOR_NODE/TARGET_IDS/SOURCE_PACKET/REFS_MANIFEST_SHA256/BASE_SHA/RISK_CLASS.
1. 해시 규약 (자가검증 완료)
targetContentHash= sha256(RFC8785-JCS 정규화한 케이스 객체).
카탈로그 로드 순서는 items.js 먼저, cases- 정렬* (tools/commercial_readiness.js dataFiles).diffHash= sha256(git diff <baseSha> <headSha>바이트).intentHash(terminology) = sha256(intentContract에서 intentHash 제외,
json sort_keys+compact, ensure_ascii=False).- content 레인의 intentHash는 CI 자동 요청서 값을 그대로 사용
(PR 본문 8필드 블록의 sha256 — gatea2a-dispatchjob이 게시). - facts 생성기 스크립트 원형: 세션 기록
/root/work/nclex-dispatch/참고
(facts*.json을 만든 node 스크립트 — fingerprint·diffHash·changedRecords 75쌍 추출).
2. terminology 레인 (ko_coverage 신규쌍 해소의 유일 경로)
- 신규 용어 후보(coverage 레코드
candidates)를 terms.json에 KMA 근거로 먼저 등록
→node tools/ko_coverage.js --build→ 후보 0 확인 → 커밋. - manifest: 스키마
nclex.terminology-bilingual-review.v1, laneterminology_bilingual, reviewer는 저자 아닌 keyring 등록 신뢰 워커,sourceBundle.files[0].content에 계약+changedRecords(75쌍)+relevantRules+
machineGate 내장.terminalBrief.notificationOwnership필수. - dry-run → 디스패치 → 브로커 감시는 스킬 동봉 워처
watch-task.sh(같은
디렉터리)로 한다. 즉석 폴링 스크립트 작성 금지 — #1389 사고(워처가completed|cancelled등 존재하지 않는 상태를 기다려 38회state=succeeded
로그를 남기고도 타임아웃, 결과 파일 미저장)의 재발 방지 계약:- 종료 상태는 브로커 TaskStatus 어휘 그대로
succeeded|failed|canceled
(비종료:blocked|queued|claimed|running; 원전 a2a-nexuspackages/broker/src/core/broker-status-predicates.ts). - 첫 폴링에서 실측 status를 출력해 어휘를 검증한다.
- 어휘 밖 상태가 3회 연속 관측되면
unknown_state로 즉시 종료·결과 저장
(성공을 놓치는 것보다 오탐 종료가 안전). - 모든 종료 경로(타임아웃·네트워크 실패 포함)에 결과 파일을 쓴다 —
결과 파일이 없으면 워처는 끝까지 살아 있었던 것이 아니다. - secret은
--secret-file(argv·출력 노출 없음, env 스타일 파일은FILE:VAR_NAME추출)로만 전달한다.
예:watch-task.sh --broker "$BROKER_URL" --task-id "$LANE_ID" \ --requester-id <manifest requester.id> --requester-role <requester.role> \ --secret-file <env-file>:BROKER_EDGE_SECRET \ --out /root/nclex-dispatch/watch-<lane>.json
- 종료 상태는 브로커 TaskStatus 어휘 그대로
review_verdict_failed이면 먼저 GET/tasks/:id와/tasks/:id/diagnostics를 본다.
(review_verdict_failed는 status가 아니라 result/error의 코드다 — 그때
status는failed로 온다. 상태 어휘와 판정 코드를 혼합하지 않는 것도 #1389 교훈.)sameSourceRedispatch.action=skip이거나negativeVerdictEvidence에
소견(findings/note)이 있으면 같은 소스 diagnostic/결과보존 재생을 하지 않는다
(#1815 item 5, #1878 + redispatch classifier). merge gate는 그대로 fail-closed.
증거가 비었거나 generic ack일 때만review: {required:false}재생으로 회수.
회수 후 수정(수정 세대 1회) → 새 head로 정식 레인 재디스패치.- PASS 후 coverage.json에 review 객체 투영(각 레코드
stage=reviewed+review{reviewer:"a2a:<node>", reviewed_at, note(태스크 id·resultHash 인용), evidence:["kuksiwon-item-writing-2018:…", "kma-terminology-6x:search=<개념>"…]})
→node tools/ko_coverage.js --check --base origin/mainPASS 확인 → 커밋·푸시.
3. CI 함정 2개
- base.sha 고착: pull_request 이벤트의 base.sha가 PR 생성 시점에 고정되어
content_process 경로 검증이 오탐하면gh api -X PUT repos/<r>/pulls/<n>/update-branch로 base 갱신(merge 커밋 추가, 콘텐츠 불변). gh run rerun은 옛 이벤트를 재생한다(수정된 PR 본문·base 미반영) —
본문 수정 후에는 빈 커밋 푸시 또는 update-branch로 새 이벤트를 만든다.
4. content 2-lane
- CI green이면 gate가 자동 요청서(A2A-EVALUATION-REQUEST 코멘트) 게시 —
head FROZEN 선언·diffHash·intentHash·레인 목록(standard 2 / high 3). 이 값이 정본. - packet: 스키마
nclex.content-pr-review.v1— contract(요청서 값+reviewerNodeId)- intentContract(요청서 intentHash) + currentTargets(전체 케이스 객체) +
structuredEvidence + evidenceExcerpts(원문 인용) + machineGate +
terminologyReview(서명 태스크 참조) + exactHeadTextIntegrity(파일 sha256).
- intentContract(요청서 intentHash) + currentTargets(전체 케이스 객체) +
- 라우팅: reviewer가 T2면 manifest를 브로커 호스트로 scp → 브로커 호스트에서
CLI 실행, T1이면 로컬 실행. 레인별 reviewer는 서로 다르고 저자 제척·keyring 등록 필수. - 핸들러 크래시(
handler_exit_nonzero)는 판정이 아니다 — 동일 packet 재시도 1회,
반복 시 다른 신뢰 워커로 재라우팅. 워커가 클레임을 안 하면 워커 노드에서a2a-hermes-worker저널 확인(취소 태스크 zombie heartbeat면 서비스 재시작 —
fleet-service 자율 범위).
4-1. review lineage 부착 (T2, a2a-nexus #2362 · #2274 scorecard)
T2 레인은 역할마다 lineage 1개에 묶는다(PR×역할). lineage는 리뷰어 1명의
흐름이라 같은 head의 역할 레인끼리 공유하면 첫 보고만 반영되고 나머지는report_out_of_state로 거부된다. 도구는 a2a-nexus main의scripts/lib/nclex-lineage-spec.mjs(#2363)와 scripts/lib/review-lineage-client.mjs.
브로커 호스트에서 실행(edge secret은 A2A_EDGE_SECRET env로만, 0절 규칙 동일)하고,
그 호스트의 nclex 체크아웃에 base·head 커밋을 fetch해 --repo로 준다.
- shallow 사본은
--repo로 쓸 수 없다 —git -C <체크아웃> rev-parse --is-shallow-repository가true면 full clone 체크아웃을 쓴다. - PR head는 브랜치명이 아니라
git -C <체크아웃> fetch origin pull/<n>/head로 받는다(받은 뒤git -C <체크아웃> rev-parse FETCH_HEAD가 요청서 headSha와 같은지 확인).
- 첫 head 디스패치 전, 레인 역할마다:
lineageId는node scripts/lib/nclex-lineage-spec.mjs --pr <n> --role <content_clinical|evidence_adversarial|high_risk_safety> \ --base <요청서 baseSha> --head <요청서 headSha> --body-file <PR 본문> --repo <nclex 체크아웃> \ --broker-url <T2 broker> --requester-id <manifest requester.id> --out spec-pr<n>-<role>.json A2A_EDGE_SECRET=... node scripts/lib/review-lineage-client.mjs create --spec spec-pr<n>-<role>.json \ --out lineage-pr<n>-<role>.json --dry-run # 확인 후 --dry-run 제거(HTTP 201 applied)nclex-pr<n>-<role>로 결정적이다 — 이미 있으면 새로 만들지 않고 기존 record를 쓴다. - manifest의 해당 역할 레인에
"reviewLineageRecord": "lineage-pr<n>-<role>.json"을
적는다(상대 경로는 manifest 디렉터리 기준, 레인 1개 = record 1개).a2a-dispatch-round.mjs --dry-run이 binding 오류를
provider 호출 전에 막는다. - 저자 수정으로 새 head가 오면 lineage를 새로 만들지 않는다:
review-lineage-client.mjs correct --record lineage-pr<n>-<role>.json --generation-ref <수정 ref> --head <new head> --repo <nclex 체크아웃>후 재리뷰 레인에 같은 record를 적는다. - bind 금지: 같은 (PR, head, 역할)에서 review-report가 이미 나간 뒤의 재실행
(결과 보존 재실행 등). 다시 bind하면 reviewer run이 부풀어 #2274 예산 데이터가
왜곡된다 — 그 레인은reviewLineageRecord없이 보낸다. 핸들러 크래시처럼 보고가
없었던 재시도는 같은 record로 bind해도 된다. - 확인: 워커 로그
"event":"review_lineage_report"(outcomereported정상,skipped|rejected|failed는 운영 노드의 일일 lineage 재측정이 알림) ·GET /review-lineages/<lineageId>.
lineage는 관측(record mode) 전용이라a2a/receipts·머지 판정에 영향이 없다.
5. receipt 게시와 마감
각 PASS 태스크의 전체 JSON에서 receipt 구성:
{task_id, head_sha, signed_head_prefix(=head 전체), lane, reviewer_node, author_node, result(<브로커 result 원본, provenance 포함>)}.PR 코멘트에 ```json 펜스로 게시 →
a2a-receipts워크플로가 keyring 검증 후
commit statusa2a/receipts를 exact-head에 투영. 확인:gh api repos/<r>/commits/<head>/status.success: "signed receipts complete: N lane(s), N reviewer(s), exact-head"
확인 후 머지는 별도 사용자 승인. main에 머지 큐가 활성돼 있어(ruleset
merge-queue-main, 2026-09-13) 승인 후 enqueue로 착지한다 —gh pr merge --squash와 relay merge helper는 "The merge strategy for main is set by the
merge queue"로 거부된다:pr_id="$(gh pr view <n> --repo <owner>/<repo> --json id --jq .id)" gh api graphql \ -f query='mutation($id:ID!){enqueuePullRequest(input:{pullRequestId:$id}){clientMutationId}}' \ -f id="$pr_id" gh pr view <n> --repo <owner>/<repo> --json state # MERGED 폴링(실패 시 큐에서 축출)a2a/receipts는 group head에도 재투영된다(a2a-receipts.yml merge_group 분기,
#535) — 큐가 이를 기다린 뒤 squash 착지. exact-head 원칙 불변: 승인 시점 head ==
enqueue 시점 head(그 사이 푸시되면 재승인). 인프라 경로(.github 등)는 gate가
아니라 ruleset이 관리하며 실패 시 ruleset 삭제가 롤백이다.
안전 규칙
- edge secret·토큰 값 출력/복사/이동 금지. 원격 브로커 작업은 그 노드에서 실행.
- head 동결 후 sync push 금지(모든 PASS 무효). 리베이스는 디스패치 전에만.
- 이 파이프라인은 GitHub 머지 승인·면허 RN 검토를 대체하지 않으며
source.verified=false·paid-pool 자격 불변.