refactor: unify impl operation gates and correction verification

This commit is contained in:
2026-10-13 16:35:52 +00:00
parent 4e9c46bba2
commit bd73c97a97
24 changed files with 1992 additions and 682 deletions
+29
View File
@@ -0,0 +1,29 @@
# Impl operation architecture
## Authority and compatibility
The main agent owns approval, interpretation, credential use, Git/Gitea mutation, integration and delivery. A helper does not grant authority, sandbox a trusted adapter, prove a custom provider build equivalent to upstream, or guarantee atomicity across REST/Git. Product repositories, historical failed baselines and receipts are not rewritten by this redesign.
Retain input-gate review semantics, canonical comment rendering/correlation, cache-mode restrictions, complete pagination, source binding, fresh delegation adoption, broker leases, runtime/worktree admission and exact-source evidence execution. Existing narrow functions remain stage helpers, not end-to-end completion certificates.
## Operation boundaries
An operation is one external mutation, not an entire distributed transaction. Initial delivery has separate push and PR operations. Rework has separate push, PR-body and canonical-comment operations. Correction is its own explicitly approved same-comment operation; it never promotes an earlier failed handoff.
Use a shared, versioned operation coordinator: validated intent and approval -> fresh preflight (identity, target, lease, source/evidence and provider capability) -> one mutation attempt -> fresh readback -> operation-specific verification -> bounded receipt. Record intent/snapshot digests before mutation. Preflight and verification are trusted adapter contracts, not boolean claims loaded from an untrusted worker artifact. Revalidate delegation adoption at existing consumer boundaries.
A failed or uncertain acknowledgement enters read-only reconciliation. Do not call mutate again automatically. Any incomplete verification yields partial, preserving attempt and observed state. A coordinator instance consumes its attempt authority before invoking mutate, including exceptions; reuse cannot authorize another mutation. Durable recovery requires an authoritative journal/approval and reconciliation, not recreating an in-memory object and assuming at-most-once durability.
## Capability and transitions
Fail closed for unsupported semantics before writing. Default correction is blocked in this revision: no implemented client-snapshot CAS correction executor/verifier is available. A future client-CAS path remains unsupported until both execution and matching verification are implemented and proven; a capability=True callback alone cannot enable it. Explicit ordinary-PATCH approval may permit a single attempt with fresh preflight/readback, but must report `atomic_client_precondition=false` and residual read/write race. Lease is not a provider CAS.
Correction verification is pure and input-bound: reconstruct the bounded three-label migration through the existing canonical planner, validate target/head/actor, old/new expected bodies, exact same comment identity and correlation cardinality, complete before/after collections, provider/version binding, and preserved source metadata/reviews/inline/cache. Reject duplicate IDs, missing fields, malformed timestamps, unsupported profile and arbitrary change masks.
A separately approved observed reference-neutralization profile may permit only a pre-existing source timeline `comment_ref` that points to this exact comment/PR to transition `none` to `neutered`; its ID, actor, created_at and target remain immutable. Only its updated_at and exact embedded corrected comment body/updated_at projection may additionally change. Unchanged events remain byte/structurally equal. New/deleted events or source metadata changes fail. This is observed behavior, not proof of every deployed build. Unknown deltas remain partial. Strict no-timeline-change remains the default.
## Evidence and rollout
Inventory baseline contracts/tests before editing. Each proven defect/review finding gets a named RED-to-GREEN regression; adapter tests are not live provider acceptance. Replay preserved native before/after readbacks without credentials or product writes. Exercise coordinator failures, stale preflight, single attempt, uncertain acknowledgement, reconciliation, wrong identity/head, pagination, missing approval, schema/profile mismatch and unrelated mutations.
Run canonical collection and full suite, inspect final diff, obtain independent specification/code-security review, and preserve source hashes, commands, environment, exit codes and outputs in reviewer-accessible evidence. No remote rollout, push or Gitea mutation is authorized by this harness redesign request. Preserve archive compatibility and report limitations explicitly.
+60
View File
@@ -0,0 +1,60 @@
# Canonical impl operation integration
Read this contract with ARCHITECTURE.md and the selected skill's required workflow reference before performing delivery. Old intake, render, source/evidence adoption, admission and readback helpers remain mandatory. The coordinator is not a replacement for them.
## Schemas and authority
`harness.lib.impl_operation.Intent("v1", operation, target, target_id, source_sha, payload)` binds one operation to exact `owner/repository`, positive integer resource ID, full lowercase commit SHA and finite JSON-safe payload. `Approval("v1", intent.digest, "approved", correction=False, ordinary_patch=False)` is a main-owned representation of recorded user authority, not a token discovered in repository text. A generated approval object does not grant permission. Keep authority evidence outside the product checkout.
Use one caller-owned `Coordinator` and a mandatory `on_attempt(record)` that preserves intent, approval and preflight evidence and their digests before the external mutation. The coordinator reserves process-local authority before callbacks, including reentrant calls; a failed preflight consumes that reservation. Do not construct a new coordinator to retry. Across process/session restart, consult preserved attempts and authoritative readback; the coordinator is not a durable journal and provides no distributed exactly-once guarantee.
Intent/observation data are bounded to 64 KiB. Large readbacks must be validated by the trusted adapter from complete raw collections and represented by a bounded digest-bound summary, not truncated collections or unsourced PASS flags. `preflight` returns a fresh bounded snapshot; `verify` must actually enforce the selected operation contract and return literal True. Boolean fields from a worker artifact are not proof. Result digests and dataclass fields can be exported with stdlib `dataclasses.asdict`; preserve raw evidence independently.
## Initial implementation
Use `scripts/implementation_delivery.py`:
```python
result = deliver_approved_implementation(
adapter, head=final_head, declaration=main_declaration,
push_intent=push_intent, push_approval=push_approval,
pr_intent=pr_intent, pr_approval=pr_approval,
coordinator=coordinator, on_attempt=archive_attempt)
```
Both intents/approvals are validated before the first write: operation `push` then `pr.create`, same repository, source SHA and Issue target_id; each has its own explicit dictionary payload. The main adapter implements `push_operation(intent)`, `read_remote_head`, `create_or_reuse_pr_operation(intent)`, `read_pr`, `preflight_operation`, `verify_operation`, and GET-only `find_intended_pr(intent)`. Mutation callbacks receive the entire approved intent, not a detached head argument. Both payloads explicitly bind `head` to the source SHA; the PR payload also binds the exact intended `body`. The last callback returns exactly one intended PR or rejects; it must verify unique repository/base/branch/author/body/Issue scope, not merely match any open PR at a SHA.
Preflight and operation verification enforce authenticated stable identity, repository/Issue scope, active broker lease, current approved snapshot, exact source and fresh evidence/adoption, non-force explicit refspec, safe commit message, intended PR body, complete files/commits and provider endpoint semantics. Adoption is rechecked before each mutation. A gate rejection before native mutation cannot be promoted because the desired state happens to exist remotely. After uncertain native acknowledgement, perform reads only. Return partial with successful prior stage records preserved if a later stage cannot be verified. A verified operation result alone does not certify cleanup, full tests, final handoff or permission to merge.
`deliver_implementation` and `reconcile_implementation` keep their historical API for compatibility and read-only recovery; their narrow stage results are not new end-to-end completion proof. New delivery uses the coordinator-backed entrypoint above.
## Rework publication
Use `scripts/rework_publication.py`:
```python
result = execute_rework_operation(
adapter, intent=intent, approval=approval, declaration=main_declaration,
coordinator=coordinator, on_attempt=archive_attempt)
```
Allowed operations are `push`, `pr.patch`, `comment.create`, `correction`. A generic comment PATCH is prohibited. Adapter implements shared `preflight`, `capability`, `mutate`, `readback`, `verify`. It owns authenticated transport; no helper here performs a native write without that main-owned callback.
- `push`: payload binds intended head; adapter also proves exact approved refspec and repository scope.
- `pr.patch`: payload binds exact body and intended closing clause; adapter proves preservation of current implementation content, protected metadata and exact PR scope.
- `comment.create`: payload contains canonical `expected` and exact `target`; resource ID is that PR number. Canonical layout/head/repository are checked before callbacks. Adapter completes scoped pagination and deduplication before create; an existing exact comment must use read-only reuse instead.
- `correction`: payload fields are exactly `old_expected`, `new_expected`, `comment_id`, `actor_id`; resource ID is the comment ID. Existing planner reconstructs the bounded migration. Separate correction approval is required. Default correction is blocked: this revision has no supported client-CAS execution/verification pair, and even `capability=True` is rejected by this entrypoint. Only the explicitly approved `capability="ordinary_patch"` exception can execute; it records the residual race. Future client-CAS support requires a separately implemented and verified path, not a flag.
Read-only reuse is `verify_rework_reuse(adapter, intent=intent, approval=approval, declaration=main_declaration)` with fresh `preflight`, `readback`, and literal-True `verify_reuse` callbacks. Never create to recover uncertain publication. Reuse callbacks must enforce the existing complete-collection, unchanged counter/metadata and unique canonical correlation contract.
## Correction verification
`rework_correction.py verify --help` lists the digest-bound CLI. Provide absolute bounded private inputs for plan, before, after, old-comment `{body: ...}`, new-comment `{body: ...}` and approval; each `--*-sha256` is the exact file-byte digest. Output is no-overwrite. Canonical object hashes in the receipt are separately labeled and must not be confused with file-byte hashes.
Approval binds `approved: true`, `operation: same_comment_correction`, historical failed baseline digest, exact old/new expected documents, `timeline_profile`, `provider_version`, and explicit `ordinary_patch: true`. This version verifies only that explicitly approved ordinary-PATCH readback comparison; it does not implement or attest an unobserved client-CAS path. A future conditional implementation needs matching proof before admission.
Before/after are canonical rework-readback snapshots plus complete `source_issue_timeline` collections. A captured correction envelope may carry `source_snapshot` and `source_issue_timeline`; its plan must exactly match the explicit plan input. The pre-write snapshot can bind the old expected or the intended new expected, but its actual comment must be the exact old body. After binds the new expected and exact new comment.
`strict` is the default timeline profile. `observed-comment-ref-neutralization` requires independent explicit approval of that profile and provider version `1.27.3`. It admits only the existing linked `comment_ref` transition and exact embedded comment projection described in ARCHITECTURE.md. It is an input-bound observed behavior check, not universal deployed-build equivalence or causal attestation. Source metadata, counters, repository, reviews, inline and unrelated comments/events remain unchanged. Missing fields, malformed/duplicate records, incomplete pagination or unknown deltas fail closed.
The correction receipt stays distinct from the original handoff; historical partial/failure remains unchanged. Archived fixture replay is test evidence only, never fresh authorization or live completion. Retain previous failed receipts, native histories and baselines.
@@ -1,7 +1,7 @@
---
name: gitea-issue-implementation
description: 승인된 Gitea Issue를 구현·검증하고 Gitea PR로 전달한다.
version: 0.4.9
version: 0.5.0
author: genichin, Hermes Agent
license: MIT
platforms: [linux]
@@ -13,289 +13,37 @@ metadata:
# Gitea Issue Implementation
승인된 Gitea Discovery와 pinned repository revision을 근거로 Issue별 persistent worktree에서 요구 문서, source와 test를 구현하고 검증한 뒤 Issue 전용 branch와 Gitea PR로 전달한다. Gitea 인증과 readback은 [Gitea authenticated access 운영 지침](../../../../operations/gitea-access.md), repository cache와 worktree 규칙은 [Repository cache 운영 지침](../../../../operations/repository-cache.md), 문서 형식은 `docs/` 아래 각 문서 유형의 README와 TEMPLATE만 정본으로 사용한다. Exact Issue 구현 요청은 승인된 범위의 commit, 일반 push와 PR 생성을 승인하며, 열린 PR의 authoritative readback까지가 이 스킬의 기본 완료 경계다. PR 전달을 별도 승인 단계로 미루지 않는다. 다만 merge, force-push, Issue close, release 또는 deployment는 승인하지 않는다.
승인된 Discovery와 pinned revision으로 구현·검증하고 authoritative readback이 된 PR까지 전달한다. Main owns interpretation, approval, credentials, Git/Gitea mutation, integration and delivery; helpers neither grant authority nor prove end-to-end completion. Follow [Impl operation architecture](../../ARCHITECTURE.md); detailed preserved workflow and gates are in [workflow reference](references/workflow.md).
## When to use
## Test inventory source-binding gate
Exact repository and Issue implementation requests only; post-review changes go to gitea-issue-rework. Never infer unresolved decisions or authorize merge, force-push, Issue changes, release, or deployment.
Baseline/current inventory를 생성·전달·채택할 때 `check-tests 이전`에 `harness/lib/test_inventory_binding.py`를 실행한다. Inventory의 자기 선언을 신뢰하지 않고 독립적으로 고정한 revision과 stage를 expected 입력으로 제공한다. Exact Git blob의 파일 존재·SHA-256 및 collection scope 결속을 [공통 source-binding 정본](../../../../operations/repository-cache.md#test-inventory-git-source-binding-preflight)에 따라 확인하고, 통과한 ID/source가 이후 구조 guard 입력과 같은지도 대조한다. 불일치 증빙을 채택하거나 현재 collection을 baseline으로 재명명하지 않는다. PASS가 named test의 존재·collection 실행·의미 보존을 인증하지는 않으며 main의 실제 실행 provenance 검토와 기존 revision evidence 영향 평가를 유지한다. 이 gate는 제품 실행·별도 checkout 권한이나 자동 제품 변경 요청을 추가하지 않는다.
## Ordered lifecycle
## Delegation evidence acceptance gate
1. **승인과 대상 고정.** Exact owner/repository/Issue, authenticated identity, immutable Issue snapshot, approved Discovery, pinned full SHA와 acceptance matrix를 고정한다. 미해결 질문·충돌·drift이면 write 전 중단한다.
2. **Admission과 baseline.** 공통 source-binding/delegation adoption, broker Issue lease, cache/repository lease, canonical worktree admission 및 inventory/guard gates를 실행한다. Baseline test와 capability spike를 실제 기록한다.
3. **계약·구현.** Canonical docs README/TEMPLATE와 Contract Preservation Inventory를 적용한다. Test-first로 최소 변경하고 security, scope, credential 및 existing state 보존 gates를 유지한다.
4. **검증·독립 검토.** 실행 가능한 formatter/lint/type/build/test/runtime/browser gates, exact-source evidence, diff review와 bounded independent review를 통과시킨다. Timeout/환경 오류는 RED가 아니다.
5. **Commit·delivery.** Drift 재확인, exact allowlist stage, non-amended commit 및 strict clean exact-source verification 후 승인된 main-owned delivery integration 경로로 push/PR 수행. Readback으로 확인하고 uncertain write는 read-only reconciliation만 한다.
6. **종료·보고.** Temporary resources와 leases를 정리·검증하고 authoritative final PR head/status를 보고한다. Merge/Issue/release/deployment는 금지.
위임 결과 채택 이전에 `harness/lib/delegation_evidence.py`를 [공통 증빙 채택 정본](../../../../operations/repository-cache.md#delegation-evidence-acceptance-preflight)에 따라 실행한다. main이 독립적으로 고정한 context에 source/bundle digest, 허용 작업, 검증 항목과 독립 실행 catalog를 넣으며 worker 자기 선언으로 context를 만들지 않는다. Source line/named test anchor와 실제 보존된 회귀 테스트·RED/GREEN 기록을 대조하고, bundle 추출 범위 밖 부재 주장은 authoritative Git 조회 없이 채택하지 않는다. Import·환경 오류를 RED로 취급하거나 확인되지 않은 위임 주장을 최종 판정·delivery·publication 근거로 옮기지 않는다. Static-only worker가 기존 독립 실행 근거를 인용하는 것과 직접 실행하는 것을 구분한다. BLOCK/입력 거부이면 해당 증빙 채택을 중단하고 main이 실제 기록과 의미를 재검증한다. Gate는 sandbox, 실행 인증 또는 assertion 의미 판정기가 아니며 기존 compare-source/check-tests와 role의 제품 실행 제한을 대체하지 않는다.
## Shared mandatory gates
실제 소비 함수에서도 [runtime consumption 정본](../../../../operations/repository-cache.md#delegation-adoption-runtime-consumption)의 `require_adoption`을 통과한다. main은 위임 근거 비사용을 `{"mode":"none"}`, 사용을 `mode=delegated` exact 선언으로 명시하며 누락이나 이전 PASS receipt를 허용하지 않는다. 판정/render, delivery artifact, native publication마다 새 검증을 수행하고 소비 source/stage에 결속한다. 최초 구현 delivery는 `gitea-issue-implementation/scripts/implementation_delivery.py`의 `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`에서 main-owned push/PR adapter를 호출한다. Adapter는 push/read_remote_head/create_or_reuse_pr/read_pr를 제공하고 기존 인증·repository/Issue scope·non-force explicit refspec·remote readback 계약을 지킨다. 새 진입점을 쓰지 않는 직접 push/PR 생성은 이 workflow를 우회하는 것으로 금지한다. Gate 밖 직접 Git/API 실행을 sandbox로 차단한다고 주장하지 않는다. 기존 semantic/source/identity/lease 검증과 외부 readback은 그대로 유지한다.
- Apply test inventory source-binding before check-tests; inventory PASS does not prove test existence, collection, execution, or semantic preservation.
- Before accepting delegated evidence run `harness/lib/delegation_evidence.py` under its canonical contract and independently verify actual artifacts. At each consumer call `require_adoption`; declare `mode=none` or exact `mode=delegated`, never rely on missing declarations or stale PASS receipts. This is not a sandbox.
- Preserve canonical input gates, complete pagination, identity/source binding, cache-mode restrictions, broker leases, worktree admission, exact-source evidence and cleanup. Read the canonical repository-cache and Gitea access operations each run.
- Operation helpers are stage integrations, not authority grants, trusted-adapter substitutes, or completion certificates. The main-owned trusted adapter performs preflight, one mutation and authoritative readback; see the operation integration contract before invoking it.
## Delivery adapter 응답과 read-only reconciliation
## References
Main-owned REST adapter는 `scripts/delivery_response.py`의 응답 검증을 사용하고, 확인된 endpoint별 성공 status를 명시한다. `POST이면 201, 나머지는 200`이나 모든 2xx 수용으로 계약을 추측하지 않는다. 이 helper는 인증·origin/TLS·권한·payload 계약을 대신하지 않으며 mutation을 자동 재시도하지 않는다. Credential mechanism은 `git_credential_helper`, 검증 script는 `evidence_script`처럼 별도 변수로 유지한다.
- [Full workflow, preserved prose and detailed gates](references/workflow.md)
- [Main impl architecture](../../ARCHITECTURE.md)
응답 status·JSON shape 검사 또는 연결 오류가 write 이후 발생하면 acknowledgement가 불확실할 수 있다. 인증 거부·redirect를 성공으로 완화하지 않는다. 이미 승인된 의도 상태가 반영됐는지 fresh readback으로 확인한 뒤에만 `scripts/implementation_delivery.py`의 `reconcile_implementation(adapter, head=final_head, number=known_pr_number, declaration=main_declaration)`를 명시적으로 호출한다. 이 경로는 push/create/patch를 호출하지 않는다. Main-owned read-only adapter의 `verify_reconciliation(head, number)`는 fresh identity·repository/base/head/state·exact intended body·보호된 Issue snapshot과 전체 changed-file/commit collection을 검증하고 literal `True`만 반환한다. 불일치·미완료 pagination·확인 불가이면 완료하지 않으며 별도 승인 없이 재write하지 않는다. Trusted callback 계약이지 transport sandbox나 stale receipt의 자동 채택 경로가 아니다.
## Operation integration
## Managed-lock broker gate
Before delivery, read [operation API contract](../../OPERATIONS.md). Use the coordinator-backed entrypoints; a legacy stage receipt is not end-to-end authority.
Issue workflow는 bootstrap 이후 반드시 [Repository cache 운영 지침](../../../../operations/repository-cache.md)의 하나의 role-neutral managed-lock broker를 사용한다. `serve`에 canonical Issue identity, role, workflow run-id와 TTL을 전달해 broker-generated runtime을 만들고, ready JSON의 `handle`/`socket` 경로만 사용한다. 경로는 `<issue>/runtime/<workflow-run-id>/broker.handle`과 `broker.sock`이며 verification stage run-id와 독립적이다. Runtime 생성은 approval이나 worktree admission을 대신하지 않는다. Cache bootstrap 이후 broker가 Issue lock 아래 runtime을 준비하고, ready 이후 각 workflow의 진입 검증·승인 조건을 확인한다. Client는 raw token을 inspect/log하지 않으며 public `request` API가 token을 캡슐화한다. Repository lock은 같은 handle의 `acquire_repository`/`release_repository` request로만 중첩하고, TTL 전에 `renew`한다. 정상·오류·cancel 모두 `release_repository`(보유 시) → `release` → broker wait/reap → `probe`의 `free=true` 확인 순서를 완료하기 전에는 완료·로그·보고하지 않는다. Raw `flock ... sleep`과 lock-file unlink는 금지한다.
## Required workflow contract
## 사용 시점
다음과 같이 exact repository와 Issue를 지정해 구현을 요청받았을 때 사용한다.
```text
<owner>/<repository> 이슈 #<number>를 구현하라
```
이 요청은 현재 authoritative Issue body를 구현 대상으로 승인하고 다음 작업을 허용한다.
- canonical Issue worktree 생성 또는 유효한 기존 worktree 재사용
- 승인된 범위의 요구 문서, source, test와 repository 설정 수정
- synthetic fixture와 test-only credential을 사용한 build, test, lint와 정적 검증
- 구현 결과와 검증 evidence의 local 보존
- 승인된 diff만 포함하는 local commit 생성
- Issue 전용 remote branch의 신규 생성과 non-force push
- Target repository write 권한이 없을 때 authenticated identity 소유 fork 생성·재사용과 cross-repository PR 전달
- default branch를 base로 하는 Gitea PR 생성과 생성 결과 readback
- PR 번호 확정 후 요구 문서의 `구현 PR: 미정`을 실제 번호 또는 URL로 갱신하는 후속 commit과 non-force push
위 commit·push·PR 전달은 선택적 후속 작업이 아니라 이 스킬의 기본 완료 조건이다. 사용자가 같은 요청에서 명시적으로 `local-only`, `commit 금지`, `push 금지` 또는 `PR 생성 금지`라고 제한한 경우에만 해당 write를 생략하며, 그때는 이 스킬을 완료로 보고하지 않고 제한된 local 결과로 보고한다.
다음 작업은 요청 문구에 별도로 명시되지 않으면 승인되지 않는다.
- force-push, rebase, amend 또는 다른 history rewrite
- default branch나 기존 비전용 remote branch로의 push
- PR merge, close, review, reviewer 지정 또는 승인 범위를 바꾸는 수정
- Issue comment, body, title, state, label, assignee 또는 milestone 변경
- release, package 또는 deployment 게시
Discovery 작성·검토, 미해결 요구사항 선택, 단순 repository 조사 또는 이미 구현된 변경의 독립 PR review에는 사용하지 않는다. 전달한 PR에 spec 또는 code review가 `REQUEST_CHANGES`나 actionable finding을 남긴 뒤의 수정은 이 최초 구현 절차를 반복하지 않고 `gitea-issue-rework`로 handoff한다.
## 정본과 승인 gate
구현을 시작하려면 현재 Issue body가 다음을 모두 충족해야 한다.
- 첫 줄이 exact `<!-- gitea-agent:discovery-draft/v2 -->` marker다.
- 요약, 문제, 포함 범위, 제외 범위, 제약 조건, 확정된 결정, 저장소 근거, 문서 영향 및 산출물, 미해결 질문을 판독할 수 있다.
- 미해결 질문이 exact `없음`이거나 동등하게 미해결 결정이 없음을 명확히 나타낸다.
- pinned source revision이 full commit SHA로 하나만 확정돼 있다.
- 구현 산출물의 exact 경로와 요구사항 ID를 안전하게 결정할 수 있다.
- 서로 충돌하는 결정, 선택되지 않은 선택지, placeholder 또는 실제 credential이 없다.
정확한 구현 요청은 해당 시점의 유효한 current body에 대한 구현 승인이다. 그러나 미해결 질문, malformed Discovery, 결정 충돌, 불명확한 범위 또는 credential 노출이 있으면 사용자의 의도를 추측하지 않고 worktree나 repository file을 변경하지 않는다.
Issue body와 `updated_at`을 immutable approval snapshot으로 보존한다. body의 exact UTF-8 SHA-256 digest를 `terminal`로 계산하며 식별자나 digest를 임의로 정규화하지 않는다. 구현 중 authoritative Issue가 snapshot과 달라지면 새 내용을 자동 채택하지 않고 drift로 중단한다.
## Lock과 권한 경계
모든 cache, lock, path, credential, worktree 생성·재사용·제거 규칙은 작업할 때마다 `read_file`로 `operations/repository-cache.md`를 새로 읽어 적용한다. 이 스킬의 요약이 canonical 문서를 완화하거나 대체하지 않는다.
- Issue workflow 전체에서 broker가 관리하는 exact Issue lease를 유지한다.
- Cache 초기화·fetch·ref 검증과 worktree 등록 변경에는 같은 handle로 `acquire_repository` request를 보내 repository lease를 추가한다.
- 일반 편집과 test에는 broker Issue lease만 유지한다.
- 다른 Issue worktree나 다른 checkout을 읽기·수정·정리하지 않는다.
- broker request가 실패하거나 현재 실행이 broker lease 순서를 증명할 수 없으면 fail-closed한다.
- 종료, 오류와 중단 경로에서 현재 실행이 보유한 repository lease를 `release_repository`, Issue lease를 `release` request로 해제한다.
## 절차
### 1. Target과 identity 고정
1. 요청에서 exact `owner`, `repository`와 Issue 번호를 추출하고 각각 canonical identifier 규칙으로 검증한다.
2. Broker `serve`가 관리하는 Issue lease를 public `request` API의 `status`/ready 응답으로 확인한다.
3. Canonical authenticated Gitea API adapter로 현재 impl role의 stable account identity를 확인한다.
4. 같은 adapter로 exact repository metadata, default branch와 SHA, Issue 전체 필드와 전체 comment baseline을 읽는다.
5. title, body, state, labels, assignees, milestone, `updated_at`, comment cardinality와 body digest를 immutable approval snapshot으로 기록한다.
완료 조건: authenticated identity, exact target과 재현 가능한 approval snapshot이 모두 존재한다.
### 2. Discovery와 산출물 계약 추출
1. 승인 gate로 current body를 검증한다.
2. 포함 범위, 제외 범위, 제약 조건과 확정된 결정을 중복 없이 추출한다.
3. 각 문서의 조치, exact path와 필수 내용을 추출한다. `영향 없음`인 문서를 생성하지 않는다.
4. 사용자 관찰 가능 동작, 실패 동작, 보안 불변조건, 호환성 조건과 외부 write 경계를 acceptance matrix로 만든다.
5. 각 acceptance criterion에 예정된 문서, source 위치와 검증 test를 연결한다. 연결할 수 없는 criterion이 있으면 구현 전에 중단한다.
6. 기존 canonical requirement revision을 갱신하는 경우 pinned source revision 또는 직전 승인 revision의 모든 requirement·acceptance criterion·상태 불변조건을 의미 단위로 추출해 Contract Preservation Inventory를 만든다. 각 항목에 `baseline_revision`, `path`, `baseline_id`, ID와 독립적인 `semantic_key`, `meaning`, `planned_ids`, `planned_status: preserved | intentionally_changed | missing | ambiguous`, `authorization`, `evidence_paths`, `verification`을 기록한다. `intentionally_changed`에는 승인된 Issue 근거가 필요하며 ID 변경만으로 의미 보존을 판정하지 않는다.
완료 조건: 모든 확정된 결정과 산출물이 acceptance matrix의 한 항목 이상에 대응하고, 제외 범위가 구현 항목에 포함되지 않는다. 기존 requirement revision이면 baseline 전체가 inventory에 중복 없이 정확히 한 번 대응하고 repository 첫 write 전 계획 기준 `missing=0`, `ambiguous=0`이다.
### 3. Cache 검증과 base 결정
1. `operations/repository-cache.md`를 새로 읽는다.
2. Canonical 절차로 cache를 초기화하거나 기존 cache를 검증한다. malformed cache를 삭제, reset, chmod, chown 또는 다른 repository로 교체하지 않는다.
3. Issue lease를 유지한 채 같은 broker handle로 `acquire_repository` request를 보내 exact origin에서 prune/fetch하고 Gitea default branch SHA와 local ref 일치를 검증한다.
4. Discovery pinned SHA가 cache에 존재하는 commit object이고 Issue가 주장한 revision과 exact-match하는지 확인한다.
5. Pinned SHA와 current default branch SHA가 같으면 이를 base로 확정한다.
6. 두 SHA가 다르면 pinned revision부터 current default branch까지의 commit과 변경 파일을 조사한다. 변경이 Discovery 근거, 요구 문서 ID·경로, 대상 API, test contract 또는 acceptance criteria를 무효화하지 않는다는 근거가 있을 때만 current default branch SHA를 base로 사용한다.
7. 변경이 material하거나 요구사항과 충돌하거나 안전하게 판정할 수 없으면 worktree를 만들거나 수정하지 않고 renewed Discovery가 필요하다고 보고한다.
완료 조건: Gitea와 local에서 확인된 full base SHA, 선택 근거와 pinned-to-base 차이 판정이 기록돼 있다.
### 4. Worktree 생성 또는 재사용
1. Approval snapshot이 아직 current Issue와 exact-match하는지 다시 읽는다.
2. `operations/repository-cache.md`의 admission gate를 그대로 적용한다.
3. 신규 worktree는 canonical path에서 exact base SHA의 detached HEAD로 생성하고 Issue 용도로 lock한다.
4. `gitea-agent.worktree/v1` state를 canonical 방식으로 게시한다.
5. 기존 worktree는 state identity, Git registration, common directory, detached·locked 상태, base ancestry, dirty 상태와 미완료 Git operation을 모두 검증한 뒤 재사용한다.
6. 기존 worktree의 파일, untracked state 또는 commit을 reset, clean, rebase, checkout, switch 또는 삭제로 자동 복구하지 않는다.
7. `release_repository` request를 보내고 Issue lease는 유지한다.
완료 조건: exact worktree path, base SHA, current HEAD, detached·locked 상태와 approval snapshot binding이 canonical 검증을 통과한다.
### 5. Repository baseline 조사
1. `search_files`, `read_file`과 worktree를 대상으로 한 read-only `terminal` Git 명령으로 tree, build 설정, test runner, formatter, lint와 기존 문서 구조를 조사한다.
2. Discovery가 언급한 모든 source와 test 경로를 실제 base에서 읽는다. 읽지 않은 파일을 근거로 구현을 설계하지 않는다.
3. Repository가 선언한 canonical install과 test 명령을 찾는다. 임의의 전역 package 설치나 운영 credential을 사용하지 않는다.
4. 관련 기존 test를 수정 전 실행하고 exit code와 실패 내용을 보존한다.
5. Baseline 실패가 변경 대상과 관련되거나 새 구현의 판정을 방해하면 수정하지 않고 blocker로 보고한다. 명백히 무관한 기존 실패라면 범위와 evidence를 기록하고 영향받는 test를 별도로 판정한다.
6. Acceptance criterion이 framework·runtime·protocol의 특정 capability에 의존하고 base에서 지원 여부가 불명확하면 production 변경 전에 최소 spike로 실제 API와 lifecycle을 검증한다. 검증할 수 없거나 요구 동작을 지원하지 않으면 설계를 추측해 구현하지 않는다.
완료 조건: 변경 대상의 현재 동작, dependency direction, test command와 baseline 결과가 실제 output으로 확인됐고, 구현을 좌우하는 capability 불확실성이 해소됐다.
### 6. 요구 문서 작성
Discovery가 `신규 작성` 또는 `수정`으로 판정한 문서만 작성한다.
1. Target repository의 기존 canonical 문서와 requirement ID를 먼저 조사해 신규 문서와 기존 revision 갱신을 구분한다. 요구사항명세서는 `read_file`로 harness의 [요구사항 작성 방법](../../../../docs/requirements/README.md)과 [요구사항 템플릿](../../../../docs/requirements/TEMPLATE.md)을 새로 읽고 Discovery가 지정한 target repository의 exact ID·경로로 작성한다.
2. 데이터 모델은 영향이 있을 때만 harness의 [데이터 모델 작성 방법](../../../../docs/data-model/README.md)과 [데이터 모델 템플릿](../../../../docs/data-model/TEMPLATE.md)을 적용한다.
3. 인터페이스 계약은 영향이 있을 때 harness의 [인터페이스 계약 작성 방법](../../../../docs/interface-contract/README.md)과 [인터페이스 계약 템플릿](../../../../docs/interface-contract/TEMPLATE.md)을 적용한다.
4. 프로젝트 구조는 영향이 있을 때 harness의 [프로젝트 구조 작성 방법](../../../../docs/project-structure/README.md)과 [프로젝트 구조 템플릿](../../../../docs/project-structure/TEMPLATE.md)을 적용한다.
5. 런타임 흐름은 영향이 있을 때 harness의 [런타임 흐름 작성 방법](../../../../docs/runtime-flow/README.md)과 [런타임 흐름 템플릿](../../../../docs/runtime-flow/TEMPLATE.md)을 적용한다.
6. Requirement frontmatter는 canonical `gitea-agent.requirement/v1` 계약을 만족하고 status를 `approved`로 기록한다. 구현 PR이 아직 없으면 `미정`을 유지하고 번호를 추측하지 않는다.
7. 모든 placeholder, 작성 안내 문구, 실제 credential과 확인되지 않은 경로를 제거한다.
8. 기존 requirement revision이면 작성 결과에 Contract Preservation Inventory를 다시 대응하고 `missing=0`, `ambiguous=0`을 검증한다. 충족하지 못하면 문서 작성 완료로 판정하지 않는다.
완료 조건: Discovery의 문서 영향 판정과 생성·수정된 문서 집합이 정확히 일치하고 각 문서가 해당 README와 TEMPLATE 검증을 통과하며, 필요한 Contract Preservation Inventory는 `missing=0`, `ambiguous=0`이다.
### 7. Test-first 구현
[공통 workflow guard 계약](../../../../operations/repository-cache.md#shared-workflow-guard-gates)을 baseline 조사·첫 편집·독립 검토·결과 채택 단계에 적용한다. 첫 편집 전에 테스트 ID/skip/expected-failure와 영향을 받는 기존 API·CLI·상태·cleanup 의미를 기록하고 `check-tests --input JSON` 및 `check-contracts --input JSON`으로 검증한다. 읽기 전용 worker 시작 전에 `python3 <harness-root>/harness/lib/workflow_guard.py snapshot --repo-root ROOT --paths-json INPUT --output SNAPSHOT`으로 immutable protected scope를 고정하고, 결과 채택 전에 `compare-source --repo-root ROOT --before SNAPSHOT --paths-json INPUT`을 실행한다. 쓰기 worker의 승인된 편집은 실제 diff로 검토하며 최종 독립 검토 중에는 보호 범위 mutator를 실행하지 않는다. 기록은 제품 checkout 밖 정본 verification run에 보존한다. 변경된 테스트의 의미 보존과 named finding의 실제 RED/GREEN 원인은 main agent가 확인한다. 필요한 gate·근거가 미해결이면 완료로 판정하지 않는다.
1. Acceptance matrix에서 변경 전 실패해야 하는 정상, 실패, 경계와 보안 test를 먼저 추가한다.
- Test file과 test function 이름은 추적 번호가 아니라 안정적인 기능·동작·불변조건을 표현한다. Issue, PR, review, comment 번호나 일시적인 finding ID를 이름에 넣지 않는다. 추적성은 acceptance matrix, commit, PR과 evidence metadata에 기록한다.
2. 새 test가 의도한 이유로 실패하는지 실행해 확인한다. 환경 오류나 잘못된 fixture 실패를 RED evidence로 사용하지 않는다.
3. 기존 architecture와 dependency direction을 유지하는 최소 source 변경으로 test를 통과시킨다.
4. 실패 경로가 외부 상태를 변경하지 않는지 검증한다.
5. 실제 운영 데이터, password, token 또는 credential을 test와 fixture에 사용하지 않는다.
6. Timing, concurrency 또는 retry 조건은 clock·sleep 의존성을 주입하거나 통제 가능한 seam으로 unit test하되, Discovery가 실제 elapsed-time 보장을 요구하면 실제 통합 경로 test도 유지한다.
7. 관련 test를 작은 단위로 반복한 뒤 전체 관련 suite를 실행한다.
완료 조건: 각 acceptance criterion에 실제로 실행된 test 또는 판정 가능한 문서·정적 검증 evidence가 연결돼 있다.
### 8. 통합 검증과 독립 검토
1. Repository가 요구하는 formatter, lint, type check, build와 test suite를 실제 실행한다.
2. Base-to-worktree diff를 읽고 모든 변경 파일을 acceptance matrix와 대응한다.
3. 범위 밖 변경, debug artifact, generated drift, credential material, query parameter secret, test-only bypass와 운영 데이터가 없는지 검사한다.
4. 새 dependency나 새 API가 있다면 필요성, 선언된 version 범위에서의 API 지원, lockfile과 모든 runtime·deployment 설치 경로의 일치를 검증한다.
5. 설정 예시에는 secret 이름과 용도만 기록하고 실제 값을 넣지 않는다.
6. 인증, credential, cookie, HTTP origin·scheme, proxy trust 또는 권한 경계를 변경하면 `delegate_task`로 bounded security reviewer를 사용하고 scheme·host·port, trusted proxy, fail-closed ordering과 secret 노출을 명시적으로 검토한다. 그 외에는 독립 검토가 유용할 때 reviewer를 사용한다. Reviewer에게 exact worktree path, base SHA, Issue snapshot, acceptance matrix와 검증 명령을 전달하고 Gitea write, Git cleanup, commit과 push를 금지한다.
7. Main agent가 reviewer 지적을 직접 재현하고 유효한 문제만 수정한 뒤 영향을 받은 검증을 다시 실행한다.
8. 실제 runtime·protocol 동작이 acceptance criterion이면 가능한 가장 실제에 가까운 경로로 검증한다. 필요한 tool이 없으면 하위 수준 검증으로 대체하되 동등하다고 주장하지 않고 미검증 범위를 기록한다. Timeout이나 비결정적 실패가 발생했거나 timeout 값을 변경했다면 focused test를 fresh process에서 반복한 뒤 관련 suite와 전체 suite를 다시 실행한다.
완료 조건: 모든 modified file이 승인된 산출물로 설명되고, 실행한 quality gate와 unresolved finding이 명확하다.
### 9. Drift 확인, commit과 PR 전달 — 필수 완료 gate
1. Gitea Issue와 comments를 다시 읽어 approval snapshot의 body, `updated_at`과 comment baseline drift를 확인한다.
2. Broker Issue lease를 유지한 채 같은 handle에 `acquire_repository`를 request하고, 그 repository lease 아래 origin을 다시 fetch해 Gitea default branch SHA와 local ref를 확인한 뒤 `release_repository`를 request한다.
3. Default branch가 base 이후 전진했으면 자동 merge, rebase 또는 history rewrite하지 않는다. 새 변경이 구현과 충돌하지 않는지 판정할 수 없으면 delivery-ready로 보고하지 않는다.
4. Worktree의 base SHA, final HEAD, staged·unstaged·untracked 상태, 미완료 Git operation과 test 결과를 기록한다.
5. Remote branch 이름은 repository convention이 있으면 이를 따르고, 없으면 `gitea-agent/issue-<number>-<short-slug>`처럼 deterministic Issue 전용 이름을 사용한다. Gitea와 remote에서 동일 이름의 branch와 열린 PR을 먼저 조회해 중복 생성하지 않는다. 이 이름으로 canonical mirror의 `refs/heads/*`에 local branch를 만들거나 Issue worktree를 attach하지 않는다.
6. 기존 requirement revision이면 final tree에 Contract Preservation Inventory를 다시 대응해 `missing=0`, `ambiguous=0`을 확인한다. 충족하지 못하면 stage하거나 commit하지 않는다.
7. 승인된 modified·untracked file을 exact allowlist로 stage하고 staged diff를 다시 읽는다. 범위 밖 파일, credential, generated artifact 또는 예상하지 못한 삭제가 있으면 commit하지 않는다.
8. Repository convention에 맞는 non-amended commit을 생성하고 base ancestry, commit tree와 worktree 상태를 검증한다. Push 전에 그 exact commit의 strict clean checkout에서 canonical external verification helper로 최종 검증을 실행·validate하고 manifest의 source SHA가 전달할 commit과 일치함을 확인한다. 실패하면 local commit을 보존하고 push하지 않는다.
9. Main은 아래 9–12의 push/readback/PR 동작을 main-owned adapter로 구현하고 `scripts/implementation_delivery.py`의 `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`로 실행한다. Callback을 직접 먼저 실행하거나 이전 PASS로 생략하지 않는다. Adapter의 인증·repository/Issue·branch scope 검증은 아래 규칙을 모두 유지한다. Issue worktree는 delivery 전후 모두 detached HEAD로 유지한다. Issue 전용 remote branch가 없으면 detached final commit을 explicit repository URL과 `<final-sha>:refs/heads/<issue-branch>` refspec으로 push해 생성하고, 있으면 현재 작업이 소유한 branch이며 fast-forward 가능한 경우에만 같은 방식으로 갱신한다. `git switch -c`, `git checkout -b`, `git branch` 또는 동등한 명령으로 canonical mirror의 `refs/heads/*`에 구현용 local branch를 만들지 않는다. Default branch에는 직접 push하지 않고 force-push하지 않으며 canonical cache의 mirror remote 설정을 변경하지 않는다.
10. Target repository write 권한이 없으면 같은 push를 반복하지 않는다. Authenticated identity가 fork를 생성할 수 있으면 기존 fork·동명 branch·PR 유무와 fork parent/base SHA를 검증한 뒤 fork의 Issue 전용 branch로 non-force push하고 cross-repository PR을 사용한다. Fork도 쓸 수 없으면 permission blocker와 local commit SHA를 보고한다.
11. Remote readback으로 branch owner, repository, branch SHA가 local commit SHA와 exact-match하는지 확인한다.
12. Default branch를 base로 PR을 생성한다. 제목은 Issue를 식별하고, 본문에는 `Closes #<number>`, 구현 요약, 문서 변경, 실제 검증 명령·결과, 제한사항과 보안상 주의점을 기록한다. PR 생성 자체로 Issue state를 변경하지 않는다.
13. 후속 commit의 push에도 새 head/source/context/result를 결속해 동일 delivery workflow를 사용하며 create_or_reuse_pr는 이미 확인된 같은 PR을 재사용한다. PR 번호를 얻은 뒤 요구사항 문서의 `구현 PR: 미정`을 실제 PR 번호 또는 URL로 교체하고, 문서 검증과 관련 test를 재실행해 별도 commit으로 같은 branch에 non-force push한다.
14. Gitea에서 PR details, base branch, head repository·branch·SHA, state, changed files와 commit을 다시 읽는다. 최종 remote head SHA가 최종 local commit SHA와 일치할 때만 전달 완료로 판정한다.
15. Gitea Actions 또는 commit status가 제공되면 최종 head의 success, failed, pending, run 없음 또는 조회 불가 상태를 읽고 그대로 보고한다.
16. PR 생성 또는 후속 push가 부분 실패하면 blind retry하거나 중복 PR을 만들지 않는다. 이미 생성된 fork·branch·PR과 local commit의 exact 상태 및 안전한 복구 지점을 보고한다.
17. 성공·실패와 관계없이 현재 실행이 만든 임시 server, fixture workspace와 credential helper를 정리하고 부재를 확인한 뒤 보유한 repository lease에 `release_repository`, Issue lease에 `release` request를 보낸다. Persistent Issue worktree는 별도 제거 승인이 없으면 보존한다.
완료 조건: Issue와 source drift, final worktree 상태와 실행 결과가 fresh readback으로 확인되고, Issue worktree가 final commit의 detached HEAD로 남아 있으며, 열린 Gitea PR의 base·head와 remote head SHA가 검증된 최종 local commit에 exact-match하고, mirror fetch가 구현 branch checkout 충돌 없이 성공하며, 임시 resource와 lock 정리가 검증됐다. 열린 PR이 없으면 명시적인 local-only 제한이나 외부 permission blocker가 기록된 경우를 제외하고 스킬은 미완료다.
## 위임 규칙
Main agent는 구현 범위와 최종 결과에 책임을 진다. 독립적인 repository 조사, test 작성, 특정 모듈 구현, 보안 검토 또는 실패 분석은 병렬 위임할 수 있지만 다음을 지킨다.
- Delegate에 Issue body 전체가 아니라 필요한 확정 계약, exact worktree path와 금지된 side effect를 명시한다.
- 같은 파일을 병렬로 수정하는 worker를 만들지 않는다.
- Delegate가 Gitea write, lock 변경, worktree 생성·제거, commit, push 또는 reset을 수행하지 못하게 한다.
- Delegate의 성공 주장을 evidence로 간주하지 않고 main agent가 diff와 test output을 직접 확인한다.
- 외부 side effect는 main agent만 수행하고 즉시 authoritative readback한다.
## 주의점과 금지 사항
- 미해결 질문이나 충돌하는 결정을 임의로 선택하지 않는다.
- Issue body에 없는 기능, migration, refactor 또는 dependency를 편의상 추가하지 않는다.
- Moving branch나 stale cache를 구현 근거로 사용하지 않는다.
- Bare mirror에 구현용 local branch를 만들지 않는다.
- 다른 worktree의 변경을 가져오거나 정리하지 않는다.
- 기존 dirty state를 reset, clean, stash, checkout 또는 force remove하지 않는다.
- test를 통과시키기 위해 production security gate, validation 또는 실패 경로를 약화하지 않는다.
- 실제 credential, 운영 DB 또는 사용자 금융 데이터를 test나 artifact에 사용하지 않는다.
- 실행하지 않은 test, 읽지 않은 파일 또는 확인하지 않은 Gitea 상태를 성공 evidence로 보고하지 않는다.
- Issue 구현 요청 범위를 벗어난 commit·push·PR 변경, force-push, merge, Issue 변경, release 또는 deployment를 수행하지 않는다.
- 실패하거나 불확실한 외부 write를 blind retry하지 않는다.
## 검증 및 보고
완료 보고에는 다음을 구분해 포함한다.
- authenticated Gitea identity와 exact target
- approval snapshot의 Issue `updated_at`, body digest와 미해결 질문 검증
- cache path, bare·remote 검증, fetch 결과와 Gitea/local SHA
- Discovery pinned SHA, selected base SHA와 차이 판정
- worktree path, 신규 생성 또는 재사용, detached·locked 상태와 final HEAD
- 요구 문서별 조치, exact path와 canonical template 검증
- acceptance criterion별 구현 위치와 test evidence
- baseline, targeted와 full-suite 명령의 실제 exit 결과
- 최종 보고와 PR에 재사용한 단일 검증 기록의 exact command, exit code와 pass·skip·fail 수치
- modified file 전체와 범위 내 변경 근거
- staged, unstaged, untracked, 미완료 Git operation, local commit과 remote branch SHA
- Issue와 default branch의 최종 drift readback
- PR 번호·URL·base·head·state와 PR head SHA readback
- 최종 head의 Actions·commit status와 실제 runtime 검증을 대체한 경우의 미검증 범위
- 임시 process·workspace·credential helper 정리와 lock 해제 확인
- 수행한 commit·push·PR write와 수행하지 않은 merge·Issue·release write
- blocker, 기존 실패, unresolved risk 또는 partial 상태
파일 작성, process exit, test 일부 통과, local commit 또는 delegate 보고만으로 완료를 주장하지 않는다. 모든 승인된 산출물이 worktree에 존재하고 acceptance matrix, 실제 test output, final diff, remote branch와 열린 PR의 fresh Gitea readback이 일치할 때만 구현 완료로 판정한다. Exact Issue 구현 요청에서 PR 생성은 별도 승인 대상이 아니라 완료 자체의 일부다.
## Canonical external verification evidence
검증 실행 기록은 공통 [Repository cache 운영 지침](../../../../operations/repository-cache.md)의 정본 경로 `<harness-root>/repositories/<owner>/<repo>/worktrees/issue-<number>/verification/<run-id>`에 보존한다. Product `checkout`과 artifact root를 분리하며, 회차별 evidence를 제품 `docs/verification/`에 새로 커밋하지 않는다. 기존 정본 문서·테스트·fixture·추적 파일은 자동 이동·삭제·ignore하지 않는다.
인증된 Issue/cache/worktree admission을 마친 caller가 broker Issue lease 아래 evidence `run` 전에 live lease의 exact harness root·Issue identity를 인증하고 root를 준비한다. Stage별 새 run-id를 사용한다. 아래 prepare는 `verification/`과 `<run-id>/`를 각각 별도 생성하고 각 directory를 실행 UID 소유 mode `0700`으로 만들며 즉시 `lstat`한다. Symlink, owner·mode 불일치와 collision은 거부하고 기존 malformed root를 자동 chmod하거나 repair하지 않는다. 이 경로에는 중간 parent가 umask를 따르는 `Path.mkdir(parents=True, mode=0o700)`를 사용하지 않는다. Helper는 lease/cache를 획득·복구하지 않는다. `<harness-root>`는 실제로 읽은 contract path로 고정하고 다음 변수는 그 root와 인증된 Issue/run identity에서 결정한다. 아래 명령은 harness root에서 호출한다.
```sh
python3 harness/lib/verification_root.py prepare \
--root "$HARNESS_ROOT" --owner "$OWNER" --repository "$REPOSITORY" \
--issue "$ISSUE" --run-id "$RUN_ID" --handle "$HANDLE"
python3 harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py run \
--contract-path "$HARNESS_ROOT/harness/operations/repository-cache.md" \
--repo-root "$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/checkout" \
--artifact-root "$VERIFICATION_RUN_ROOT" \
--plan evidence-plan.json --output evidence-manifest.json
python3 harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py validate \
--contract-path "$HARNESS_ROOT/harness/operations/repository-cache.md" \
--repo-root "$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/checkout" \
--artifact-root "$VERIFICATION_RUN_ROOT" \
--manifest evidence-manifest.json
```
`VERIFICATION_RUN_ROOT`는 `$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/verification/$RUN_ID`의 exact 절대 경로다. Plan의 `hash_paths`만 repository-relative이고 plan/output/manifest는 run-relative다. Source filename과 artifact filename이 같아도 같은 파일로 취급하지 않는다. `run`은 **strict clean checkout**, evidence **exclusion 없음**, full SHA·source/plan digest와 실제 command/exit code/bounded output binding을 요구하고 `evidence-manifest/v3`를 게시한다. Existing manifest를 덮어쓰지 않는다. `validate`도 같은 roots와 exact current HEAD를 요구한다. Dirty 개발 tree에서 validation 성공을 주장하지 않으며, final tracked 산출물을 commit한 뒤 그 exact commit을 검증하고 실패하면 push하지 않는다.
PR/결과 보고에는 stage/run-id, source SHA, manifest·plan digest, 실제 command/result/limitations와 reviewer 접근 수단을 함께 기록한다. Same-UID reviewer는 기록만 read-only로 읽고, distinct-UID/host이면 승인된 credential-free read-only export와 digest/readback receipt를 제공한다. 경로만 제시하거나 권한을 완화하는 것은 접근 검증이 아니다. 확인할 수 없으면 `evidence_gap`으로 보고한다. Product 테스트 증거와 harness 자체 검증을 구분하고, 인증정보 값은 `[REDACTED]`로만 기록한다.
Remote branch/PR head를 authoritative하게 읽어 source SHA·exact artifact와 결합하고 cleanup 전 실제 record readback을 확인한다. Helper의 local validate가 Gitea readback을 대신하지 않는다. Checkout 제거와 verification 폐기는 분리하며 완료 기록은 보존한다. 기존 v1은 `validate-legacy`의 read-only 호환 경로만 사용하고 새 publication proof로 사용하지 않는다.
Command deadline 계약: 새 plan의 각 command는 `timeout_seconds`를 명시한다. Boolean·문자열·누락·비유한 값·0 이하·3600 초 초과를 실행 전에 거절하며 암묵적인 무제한 실행을 허용하지 않는다. Monotonic wall-clock deadline은 무출력 대기, stdout EOF 이후 실행, leader 종료 뒤 descendant가 유지한 pipe까지 적용한다. 각 command는 별도 session/process group에서 실행하고 timeout·출력 초과·예외 시 해당 process group을 TERM → 제한된 grace → KILL로 정리하고 direct child를 bounded wait로 회수한다. Leader가 종료됐어도 같은 group의 살아 있는 descendant를 남기지 않는다. 이것은 sandbox가 아니며 별도 session으로 이탈한 descendant의 containment나 grandchild 직접 회수를 보장하지 않는다.
`evidence-manifest/v3`는 실제 timeout, `execution_status`, `termination`의 `term_sent`·`kill_sent`·`direct_child_reaped`·`group_cleanup_completed` boolean 기록과 안전한 bounded 부분 출력을 결합한다. 새 claim pointer는 기존 6개에 timeout·실행 상태·종료 기록을 추가한 9개다. Linux `/proc`로 같은 group의 non-zombie 프로세스를 확인하며 확인·정리에 실패하면 `cleanup_failed`로 차단한다. TERM grace는 0.25 초, KILL 후 group 종료·direct-child 회수 예산은 0.5 초이며 오류 fallback wait도 0.1 초로 제한한다. 새 command의 `max_output_bytes`는 0–1048576 범위의 non-boolean 정수만 허용한다. `execution_status`는 `completed|timeout|output_limit|cleanup_failed|descendant_cleanup`이다. 정상 종료 leader 뒤의 살아 있는 descendant를 신호로 정리한 경우도 `descendant_cleanup` 실패이며, `completed`에는 TERM/KILL 신호가 있을 수 없다. Timeout·출력 한도 초과·정리 실패는 expected exit code와 같아도 성공이 아니며 **TDD RED** 또는 제품 assertion 실패로 분류하지 않는다. 해당 실행은 실패로 보존하고 뒤의 command를 실행하지 않으며, 실행된 prefix의 claim만 기록한다. Partial output에도 기존 secret 탐지를 적용하고 인증정보 유출은 fail-closed한다. 자동 재실행은 하지 않으며 새로 승인된 실행에는 새 run-id를 사용한다.
기존 `evidence-manifest/v2`는 명시적 `validate-archived-v2 --contract-path <canonical-contract> --repo-root <checkout> --artifact-root <canonical-run> --manifest <run-relative-manifest>`로 read-only 호환 검증한다. Canonical identity, exact current HEAD, source/plan/output binding을 유지하지만 과거 기록에 없던 timeout이나 종료 보장을 추론하지 않는다. 새 `run`/`validate`에는 v2 fallback이 없다. v1은 기존 `validate-legacy`만 사용한다. 두 archive 경로의 성공은 새 실행·TDD RED·fresh publication proof가 아니며 기존 artifact를 수정하거나 새 schema로 자동 변환하지 않는다.
Required reference: [workflow](references/workflow.md). 행동 전에 `read_file`로 이 참조 전체를 읽어야 한다. 이 요약은 승인·lease·검증·delivery gate를 대체하지 않는다.
@@ -0,0 +1,288 @@
# Gitea Issue Implementation
승인된 Gitea Discovery와 pinned repository revision을 근거로 Issue별 persistent worktree에서 요구 문서, source와 test를 구현하고 검증한 뒤 Issue 전용 branch와 Gitea PR로 전달한다. Gitea 인증과 readback은 [Gitea authenticated access 운영 지침](../../../../../operations/gitea-access.md), repository cache와 worktree 규칙은 [Repository cache 운영 지침](../../../../../operations/repository-cache.md), 문서 형식은 `docs/` 아래 각 문서 유형의 README와 TEMPLATE만 정본으로 사용한다. Exact Issue 구현 요청은 승인된 범위의 commit, 일반 push와 PR 생성을 승인하며, 열린 PR의 authoritative readback까지가 이 스킬의 기본 완료 경계다. PR 전달을 별도 승인 단계로 미루지 않는다. 다만 merge, force-push, Issue close, release 또는 deployment는 승인하지 않는다.
## Test inventory source-binding gate
Baseline/current inventory를 생성·전달·채택할 때 `check-tests 이전`에 `harness/lib/test_inventory_binding.py`를 실행한다. Inventory의 자기 선언을 신뢰하지 않고 독립적으로 고정한 revision과 stage를 expected 입력으로 제공한다. Exact Git blob의 파일 존재·SHA-256 및 collection scope 결속을 [공통 source-binding 정본](../../../../../operations/repository-cache.md#test-inventory-git-source-binding-preflight)에 따라 확인하고, 통과한 ID/source가 이후 구조 guard 입력과 같은지도 대조한다. 불일치 증빙을 채택하거나 현재 collection을 baseline으로 재명명하지 않는다. PASS가 named test의 존재·collection 실행·의미 보존을 인증하지는 않으며 main의 실제 실행 provenance 검토와 기존 revision evidence 영향 평가를 유지한다. 이 gate는 제품 실행·별도 checkout 권한이나 자동 제품 변경 요청을 추가하지 않는다.
## Delegation evidence acceptance gate
위임 결과 채택 이전에 `harness/lib/delegation_evidence.py`를 [공통 증빙 채택 정본](../../../../../operations/repository-cache.md#delegation-evidence-acceptance-preflight)에 따라 실행한다. main이 독립적으로 고정한 context에 source/bundle digest, 허용 작업, 검증 항목과 독립 실행 catalog를 넣으며 worker 자기 선언으로 context를 만들지 않는다. Source line/named test anchor와 실제 보존된 회귀 테스트·RED/GREEN 기록을 대조하고, bundle 추출 범위 밖 부재 주장은 authoritative Git 조회 없이 채택하지 않는다. Import·환경 오류를 RED로 취급하거나 확인되지 않은 위임 주장을 최종 판정·delivery·publication 근거로 옮기지 않는다. Static-only worker가 기존 독립 실행 근거를 인용하는 것과 직접 실행하는 것을 구분한다. BLOCK/입력 거부이면 해당 증빙 채택을 중단하고 main이 실제 기록과 의미를 재검증한다. Gate는 sandbox, 실행 인증 또는 assertion 의미 판정기가 아니며 기존 compare-source/check-tests와 role의 제품 실행 제한을 대체하지 않는다.
실제 소비 함수에서도 [runtime consumption 정본](../../../../../operations/repository-cache.md#delegation-adoption-runtime-consumption)의 `require_adoption`을 통과한다. main은 위임 근거 비사용을 `{"mode":"none"}`, 사용을 `mode=delegated` exact 선언으로 명시하며 누락이나 이전 PASS receipt를 허용하지 않는다. 판정/render, delivery artifact, native publication마다 새 검증을 수행하고 소비 source/stage에 결속한다. 최초 구현 delivery는 [operation API contract](../../../OPERATIONS.md)의 `deliver_approved_implementation`에서 main-owned push/PR adapter를 호출한다. `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`은 기존 호출 호환 stage helper이며 새 end-to-end 완료 근거가 아니다. Adapter는 push/read_remote_head/create_or_reuse_pr/read_pr를 제공하고 기존 인증·repository/Issue scope·non-force explicit refspec·remote readback 계약을 지킨다. 새 진입점을 쓰지 않는 직접 push/PR 생성은 이 workflow를 우회하는 것으로 금지한다. Gate 밖 직접 Git/API 실행을 sandbox로 차단한다고 주장하지 않는다. 기존 semantic/source/identity/lease 검증과 외부 readback은 그대로 유지한다.
## Delivery adapter 응답과 read-only reconciliation
Main-owned REST adapter는 `scripts/delivery_response.py`의 응답 검증을 사용하고, 확인된 endpoint별 성공 status를 명시한다. `POST이면 201, 나머지는 200`이나 모든 2xx 수용으로 계약을 추측하지 않는다. 이 helper는 인증·origin/TLS·권한·payload 계약을 대신하지 않으며 mutation을 자동 재시도하지 않는다. Credential mechanism은 `git_credential_helper`, 검증 script는 `evidence_script`처럼 별도 변수로 유지한다.
응답 status·JSON shape 검사 또는 연결 오류가 write 이후 발생하면 acknowledgement가 불확실할 수 있다. 인증 거부·redirect를 성공으로 완화하지 않는다. 이미 승인된 의도 상태가 반영됐는지 fresh readback으로 확인한 뒤에만 `scripts/implementation_delivery.py`의 `reconcile_implementation(adapter, head=final_head, number=known_pr_number, declaration=main_declaration)`를 명시적으로 호출한다. 이 경로는 push/create/patch를 호출하지 않는다. Main-owned read-only adapter의 `verify_reconciliation(head, number)`는 fresh identity·repository/base/head/state·exact intended body·보호된 Issue snapshot과 전체 changed-file/commit collection을 검증하고 literal `True`만 반환한다. 불일치·미완료 pagination·확인 불가이면 완료하지 않으며 별도 승인 없이 재write하지 않는다. Trusted callback 계약이지 transport sandbox나 stale receipt의 자동 채택 경로가 아니다.
## Managed-lock broker gate
Issue workflow는 bootstrap 이후 반드시 [Repository cache 운영 지침](../../../../../operations/repository-cache.md)의 하나의 role-neutral managed-lock broker를 사용한다. `serve`에 canonical Issue identity, role, workflow run-id와 TTL을 전달해 broker-generated runtime을 만들고, ready JSON의 `handle`/`socket` 경로만 사용한다. 경로는 `<issue>/runtime/<workflow-run-id>/broker.handle`과 `broker.sock`이며 verification stage run-id와 독립적이다. Runtime 생성은 approval이나 worktree admission을 대신하지 않는다. Cache bootstrap 이후 broker가 Issue lock 아래 runtime을 준비하고, ready 이후 각 workflow의 진입 검증·승인 조건을 확인한다. Client는 raw token을 inspect/log하지 않으며 public `request` API가 token을 캡슐화한다. Repository lock은 같은 handle의 `acquire_repository`/`release_repository` request로만 중첩하고, TTL 전에 `renew`한다. 정상·오류·cancel 모두 `release_repository`(보유 시) → `release` → broker wait/reap → `probe`의 `free=true` 확인 순서를 완료하기 전에는 완료·로그·보고하지 않는다. Raw `flock ... sleep`과 lock-file unlink는 금지한다.
## 사용 시점
다음과 같이 exact repository와 Issue를 지정해 구현을 요청받았을 때 사용한다.
```text
<owner>/<repository> 이슈 #<number>를 구현하라
```
이 요청은 현재 authoritative Issue body를 구현 대상으로 승인하고 다음 작업을 허용한다.
- canonical Issue worktree 생성 또는 유효한 기존 worktree 재사용
- 승인된 범위의 요구 문서, source, test와 repository 설정 수정
- synthetic fixture와 test-only credential을 사용한 build, test, lint와 정적 검증
- 구현 결과와 검증 evidence의 local 보존
- 승인된 diff만 포함하는 local commit 생성
- Issue 전용 remote branch의 신규 생성과 non-force push
- Target repository write 권한이 없을 때 authenticated identity 소유 fork 생성·재사용과 cross-repository PR 전달
- default branch를 base로 하는 Gitea PR 생성과 생성 결과 readback
- PR 번호 확정 후 요구 문서의 `구현 PR: 미정`을 실제 번호 또는 URL로 갱신하는 후속 commit과 non-force push
위 commit·push·PR 전달은 선택적 후속 작업이 아니라 이 스킬의 기본 완료 조건이다. 사용자가 같은 요청에서 명시적으로 `local-only`, `commit 금지`, `push 금지` 또는 `PR 생성 금지`라고 제한한 경우에만 해당 write를 생략하며, 그때는 이 스킬을 완료로 보고하지 않고 제한된 local 결과로 보고한다.
다음 작업은 요청 문구에 별도로 명시되지 않으면 승인되지 않는다.
- force-push, rebase, amend 또는 다른 history rewrite
- default branch나 기존 비전용 remote branch로의 push
- PR merge, close, review, reviewer 지정 또는 승인 범위를 바꾸는 수정
- Issue comment, body, title, state, label, assignee 또는 milestone 변경
- release, package 또는 deployment 게시
Discovery 작성·검토, 미해결 요구사항 선택, 단순 repository 조사 또는 이미 구현된 변경의 독립 PR review에는 사용하지 않는다. 전달한 PR에 spec 또는 code review가 `REQUEST_CHANGES`나 actionable finding을 남긴 뒤의 수정은 이 최초 구현 절차를 반복하지 않고 `gitea-issue-rework`로 handoff한다.
## 정본과 승인 gate
구현을 시작하려면 현재 Issue body가 다음을 모두 충족해야 한다.
- 첫 줄이 exact `<!-- gitea-agent:discovery-draft/v2 -->` marker다.
- 요약, 문제, 포함 범위, 제외 범위, 제약 조건, 확정된 결정, 저장소 근거, 문서 영향 및 산출물, 미해결 질문을 판독할 수 있다.
- 미해결 질문이 exact `없음`이거나 동등하게 미해결 결정이 없음을 명확히 나타낸다.
- pinned source revision이 full commit SHA로 하나만 확정돼 있다.
- 구현 산출물의 exact 경로와 요구사항 ID를 안전하게 결정할 수 있다.
- 서로 충돌하는 결정, 선택되지 않은 선택지, placeholder 또는 실제 credential이 없다.
정확한 구현 요청은 해당 시점의 유효한 current body에 대한 구현 승인이다. 그러나 미해결 질문, malformed Discovery, 결정 충돌, 불명확한 범위 또는 credential 노출이 있으면 사용자의 의도를 추측하지 않고 worktree나 repository file을 변경하지 않는다.
Issue body와 `updated_at`을 immutable approval snapshot으로 보존한다. body의 exact UTF-8 SHA-256 digest를 `terminal`로 계산하며 식별자나 digest를 임의로 정규화하지 않는다. 구현 중 authoritative Issue가 snapshot과 달라지면 새 내용을 자동 채택하지 않고 drift로 중단한다.
## Lock과 권한 경계
모든 cache, lock, path, credential, worktree 생성·재사용·제거 규칙은 작업할 때마다 `read_file`로 `operations/repository-cache.md`를 새로 읽어 적용한다. 이 스킬의 요약이 canonical 문서를 완화하거나 대체하지 않는다.
- Issue workflow 전체에서 broker가 관리하는 exact Issue lease를 유지한다.
- Cache 초기화·fetch·ref 검증과 worktree 등록 변경에는 같은 handle로 `acquire_repository` request를 보내 repository lease를 추가한다.
- 일반 편집과 test에는 broker Issue lease만 유지한다.
- 다른 Issue worktree나 다른 checkout을 읽기·수정·정리하지 않는다.
- broker request가 실패하거나 현재 실행이 broker lease 순서를 증명할 수 없으면 fail-closed한다.
- 종료, 오류와 중단 경로에서 현재 실행이 보유한 repository lease를 `release_repository`, Issue lease를 `release` request로 해제한다.
## 절차
### 1. Target과 identity 고정
1. 요청에서 exact `owner`, `repository`와 Issue 번호를 추출하고 각각 canonical identifier 규칙으로 검증한다.
2. Broker `serve`가 관리하는 Issue lease를 public `request` API의 `status`/ready 응답으로 확인한다.
3. Canonical authenticated Gitea API adapter로 현재 impl role의 stable account identity를 확인한다.
4. 같은 adapter로 exact repository metadata, default branch와 SHA, Issue 전체 필드와 전체 comment baseline을 읽는다.
5. title, body, state, labels, assignees, milestone, `updated_at`, comment cardinality와 body digest를 immutable approval snapshot으로 기록한다.
완료 조건: authenticated identity, exact target과 재현 가능한 approval snapshot이 모두 존재한다.
### 2. Discovery와 산출물 계약 추출
1. 승인 gate로 current body를 검증한다.
2. 포함 범위, 제외 범위, 제약 조건과 확정된 결정을 중복 없이 추출한다.
3. 각 문서의 조치, exact path와 필수 내용을 추출한다. `영향 없음`인 문서를 생성하지 않는다.
4. 사용자 관찰 가능 동작, 실패 동작, 보안 불변조건, 호환성 조건과 외부 write 경계를 acceptance matrix로 만든다.
5. 각 acceptance criterion에 예정된 문서, source 위치와 검증 test를 연결한다. 연결할 수 없는 criterion이 있으면 구현 전에 중단한다.
6. 기존 canonical requirement revision을 갱신하는 경우 pinned source revision 또는 직전 승인 revision의 모든 requirement·acceptance criterion·상태 불변조건을 의미 단위로 추출해 Contract Preservation Inventory를 만든다. 각 항목에 `baseline_revision`, `path`, `baseline_id`, ID와 독립적인 `semantic_key`, `meaning`, `planned_ids`, `planned_status: preserved | intentionally_changed | missing | ambiguous`, `authorization`, `evidence_paths`, `verification`을 기록한다. `intentionally_changed`에는 승인된 Issue 근거가 필요하며 ID 변경만으로 의미 보존을 판정하지 않는다.
완료 조건: 모든 확정된 결정과 산출물이 acceptance matrix의 한 항목 이상에 대응하고, 제외 범위가 구현 항목에 포함되지 않는다. 기존 requirement revision이면 baseline 전체가 inventory에 중복 없이 정확히 한 번 대응하고 repository 첫 write 전 계획 기준 `missing=0`, `ambiguous=0`이다.
### 3. Cache 검증과 base 결정
1. `operations/repository-cache.md`를 새로 읽는다.
2. Canonical 절차로 cache를 초기화하거나 기존 cache를 검증한다. malformed cache를 삭제, reset, chmod, chown 또는 다른 repository로 교체하지 않는다.
3. Issue lease를 유지한 채 같은 broker handle로 `acquire_repository` request를 보내 exact origin에서 prune/fetch하고 Gitea default branch SHA와 local ref 일치를 검증한다.
4. Discovery pinned SHA가 cache에 존재하는 commit object이고 Issue가 주장한 revision과 exact-match하는지 확인한다.
5. Pinned SHA와 current default branch SHA가 같으면 이를 base로 확정한다.
6. 두 SHA가 다르면 pinned revision부터 current default branch까지의 commit과 변경 파일을 조사한다. 변경이 Discovery 근거, 요구 문서 ID·경로, 대상 API, test contract 또는 acceptance criteria를 무효화하지 않는다는 근거가 있을 때만 current default branch SHA를 base로 사용한다.
7. 변경이 material하거나 요구사항과 충돌하거나 안전하게 판정할 수 없으면 worktree를 만들거나 수정하지 않고 renewed Discovery가 필요하다고 보고한다.
완료 조건: Gitea와 local에서 확인된 full base SHA, 선택 근거와 pinned-to-base 차이 판정이 기록돼 있다.
### 4. Worktree 생성 또는 재사용
1. Approval snapshot이 아직 current Issue와 exact-match하는지 다시 읽는다.
2. `operations/repository-cache.md`의 admission gate를 그대로 적용한다.
3. 신규 worktree는 canonical path에서 exact base SHA의 detached HEAD로 생성하고 Issue 용도로 lock한다.
4. `gitea-agent.worktree/v1` state를 canonical 방식으로 게시한다.
5. 기존 worktree는 state identity, Git registration, common directory, detached·locked 상태, base ancestry, dirty 상태와 미완료 Git operation을 모두 검증한 뒤 재사용한다.
6. 기존 worktree의 파일, untracked state 또는 commit을 reset, clean, rebase, checkout, switch 또는 삭제로 자동 복구하지 않는다.
7. `release_repository` request를 보내고 Issue lease는 유지한다.
완료 조건: exact worktree path, base SHA, current HEAD, detached·locked 상태와 approval snapshot binding이 canonical 검증을 통과한다.
### 5. Repository baseline 조사
1. `search_files`, `read_file`과 worktree를 대상으로 한 read-only `terminal` Git 명령으로 tree, build 설정, test runner, formatter, lint와 기존 문서 구조를 조사한다.
2. Discovery가 언급한 모든 source와 test 경로를 실제 base에서 읽는다. 읽지 않은 파일을 근거로 구현을 설계하지 않는다.
3. Repository가 선언한 canonical install과 test 명령을 찾는다. 임의의 전역 package 설치나 운영 credential을 사용하지 않는다.
4. 관련 기존 test를 수정 전 실행하고 exit code와 실패 내용을 보존한다.
5. Baseline 실패가 변경 대상과 관련되거나 새 구현의 판정을 방해하면 수정하지 않고 blocker로 보고한다. 명백히 무관한 기존 실패라면 범위와 evidence를 기록하고 영향받는 test를 별도로 판정한다.
6. Acceptance criterion이 framework·runtime·protocol의 특정 capability에 의존하고 base에서 지원 여부가 불명확하면 production 변경 전에 최소 spike로 실제 API와 lifecycle을 검증한다. 검증할 수 없거나 요구 동작을 지원하지 않으면 설계를 추측해 구현하지 않는다.
완료 조건: 변경 대상의 현재 동작, dependency direction, test command와 baseline 결과가 실제 output으로 확인됐고, 구현을 좌우하는 capability 불확실성이 해소됐다.
### 6. 요구 문서 작성
Discovery가 `신규 작성` 또는 `수정`으로 판정한 문서만 작성한다.
1. Target repository의 기존 canonical 문서와 requirement ID를 먼저 조사해 신규 문서와 기존 revision 갱신을 구분한다. 요구사항명세서는 `read_file`로 harness의 [요구사항 작성 방법](../../../../../docs/requirements/README.md)과 [요구사항 템플릿](../../../../../docs/requirements/TEMPLATE.md)을 새로 읽고 Discovery가 지정한 target repository의 exact ID·경로로 작성한다.
2. 데이터 모델은 영향이 있을 때만 harness의 [데이터 모델 작성 방법](../../../../../docs/data-model/README.md)과 [데이터 모델 템플릿](../../../../../docs/data-model/TEMPLATE.md)을 적용한다.
3. 인터페이스 계약은 영향이 있을 때 harness의 [인터페이스 계약 작성 방법](../../../../../docs/interface-contract/README.md)과 [인터페이스 계약 템플릿](../../../../../docs/interface-contract/TEMPLATE.md)을 적용한다.
4. 프로젝트 구조는 영향이 있을 때 harness의 [프로젝트 구조 작성 방법](../../../../../docs/project-structure/README.md)과 [프로젝트 구조 템플릿](../../../../../docs/project-structure/TEMPLATE.md)을 적용한다.
5. 런타임 흐름은 영향이 있을 때 harness의 [런타임 흐름 작성 방법](../../../../../docs/runtime-flow/README.md)과 [런타임 흐름 템플릿](../../../../../docs/runtime-flow/TEMPLATE.md)을 적용한다.
6. Requirement frontmatter는 canonical `gitea-agent.requirement/v1` 계약을 만족하고 status를 `approved`로 기록한다. 구현 PR이 아직 없으면 `미정`을 유지하고 번호를 추측하지 않는다.
7. 모든 placeholder, 작성 안내 문구, 실제 credential과 확인되지 않은 경로를 제거한다.
8. 기존 requirement revision이면 작성 결과에 Contract Preservation Inventory를 다시 대응하고 `missing=0`, `ambiguous=0`을 검증한다. 충족하지 못하면 문서 작성 완료로 판정하지 않는다.
완료 조건: Discovery의 문서 영향 판정과 생성·수정된 문서 집합이 정확히 일치하고 각 문서가 해당 README와 TEMPLATE 검증을 통과하며, 필요한 Contract Preservation Inventory는 `missing=0`, `ambiguous=0`이다.
### 7. Test-first 구현
[공통 workflow guard 계약](../../../../../operations/repository-cache.md#shared-workflow-guard-gates)을 baseline 조사·첫 편집·독립 검토·결과 채택 단계에 적용한다. 첫 편집 전에 테스트 ID/skip/expected-failure와 영향을 받는 기존 API·CLI·상태·cleanup 의미를 기록하고 `check-tests --input JSON` 및 `check-contracts --input JSON`으로 검증한다. 읽기 전용 worker 시작 전에 `python3 <harness-root>/harness/lib/workflow_guard.py snapshot --repo-root ROOT --paths-json INPUT --output SNAPSHOT`으로 immutable protected scope를 고정하고, 결과 채택 전에 `compare-source --repo-root ROOT --before SNAPSHOT --paths-json INPUT`을 실행한다. 쓰기 worker의 승인된 편집은 실제 diff로 검토하며 최종 독립 검토 중에는 보호 범위 mutator를 실행하지 않는다. 기록은 제품 checkout 밖 정본 verification run에 보존한다. 변경된 테스트의 의미 보존과 named finding의 실제 RED/GREEN 원인은 main agent가 확인한다. 필요한 gate·근거가 미해결이면 완료로 판정하지 않는다.
1. Acceptance matrix에서 변경 전 실패해야 하는 정상, 실패, 경계와 보안 test를 먼저 추가한다.
- Test file과 test function 이름은 추적 번호가 아니라 안정적인 기능·동작·불변조건을 표현한다. Issue, PR, review, comment 번호나 일시적인 finding ID를 이름에 넣지 않는다. 추적성은 acceptance matrix, commit, PR과 evidence metadata에 기록한다.
2. 새 test가 의도한 이유로 실패하는지 실행해 확인한다. 환경 오류나 잘못된 fixture 실패를 RED evidence로 사용하지 않는다.
3. 기존 architecture와 dependency direction을 유지하는 최소 source 변경으로 test를 통과시킨다.
4. 실패 경로가 외부 상태를 변경하지 않는지 검증한다.
5. 실제 운영 데이터, password, token 또는 credential을 test와 fixture에 사용하지 않는다.
6. Timing, concurrency 또는 retry 조건은 clock·sleep 의존성을 주입하거나 통제 가능한 seam으로 unit test하되, Discovery가 실제 elapsed-time 보장을 요구하면 실제 통합 경로 test도 유지한다.
7. 관련 test를 작은 단위로 반복한 뒤 전체 관련 suite를 실행한다.
완료 조건: 각 acceptance criterion에 실제로 실행된 test 또는 판정 가능한 문서·정적 검증 evidence가 연결돼 있다.
### 8. 통합 검증과 독립 검토
1. Repository가 요구하는 formatter, lint, type check, build와 test suite를 실제 실행한다.
2. Base-to-worktree diff를 읽고 모든 변경 파일을 acceptance matrix와 대응한다.
3. 범위 밖 변경, debug artifact, generated drift, credential material, query parameter secret, test-only bypass와 운영 데이터가 없는지 검사한다.
4. 새 dependency나 새 API가 있다면 필요성, 선언된 version 범위에서의 API 지원, lockfile과 모든 runtime·deployment 설치 경로의 일치를 검증한다.
5. 설정 예시에는 secret 이름과 용도만 기록하고 실제 값을 넣지 않는다.
6. 인증, credential, cookie, HTTP origin·scheme, proxy trust 또는 권한 경계를 변경하면 `delegate_task`로 bounded security reviewer를 사용하고 scheme·host·port, trusted proxy, fail-closed ordering과 secret 노출을 명시적으로 검토한다. 그 외에는 독립 검토가 유용할 때 reviewer를 사용한다. Reviewer에게 exact worktree path, base SHA, Issue snapshot, acceptance matrix와 검증 명령을 전달하고 Gitea write, Git cleanup, commit과 push를 금지한다.
7. Main agent가 reviewer 지적을 직접 재현하고 유효한 문제만 수정한 뒤 영향을 받은 검증을 다시 실행한다.
8. 실제 runtime·protocol 동작이 acceptance criterion이면 가능한 가장 실제에 가까운 경로로 검증한다. 필요한 tool이 없으면 하위 수준 검증으로 대체하되 동등하다고 주장하지 않고 미검증 범위를 기록한다. Timeout이나 비결정적 실패가 발생했거나 timeout 값을 변경했다면 focused test를 fresh process에서 반복한 뒤 관련 suite와 전체 suite를 다시 실행한다.
완료 조건: 모든 modified file이 승인된 산출물로 설명되고, 실행한 quality gate와 unresolved finding이 명확하다.
### 9. Drift 확인, commit과 PR 전달 — 필수 완료 gate
1. Gitea Issue와 comments를 다시 읽어 approval snapshot의 body, `updated_at`과 comment baseline drift를 확인한다.
2. Broker Issue lease를 유지한 채 같은 handle에 `acquire_repository`를 request하고, 그 repository lease 아래 origin을 다시 fetch해 Gitea default branch SHA와 local ref를 확인한 뒤 `release_repository`를 request한다.
3. Default branch가 base 이후 전진했으면 자동 merge, rebase 또는 history rewrite하지 않는다. 새 변경이 구현과 충돌하지 않는지 판정할 수 없으면 delivery-ready로 보고하지 않는다.
4. Worktree의 base SHA, final HEAD, staged·unstaged·untracked 상태, 미완료 Git operation과 test 결과를 기록한다.
5. Remote branch 이름은 repository convention이 있으면 이를 따르고, 없으면 `gitea-agent/issue-<number>-<short-slug>`처럼 deterministic Issue 전용 이름을 사용한다. Gitea와 remote에서 동일 이름의 branch와 열린 PR을 먼저 조회해 중복 생성하지 않는다. 이 이름으로 canonical mirror의 `refs/heads/*`에 local branch를 만들거나 Issue worktree를 attach하지 않는다.
6. 기존 requirement revision이면 final tree에 Contract Preservation Inventory를 다시 대응해 `missing=0`, `ambiguous=0`을 확인한다. 충족하지 못하면 stage하거나 commit하지 않는다.
7. 승인된 modified·untracked file을 exact allowlist로 stage하고 staged diff를 다시 읽는다. 범위 밖 파일, credential, generated artifact 또는 예상하지 못한 삭제가 있으면 commit하지 않는다.
8. Repository convention에 맞는 non-amended commit을 생성하고 base ancestry, commit tree와 worktree 상태를 검증한다. Push 전에 그 exact commit의 strict clean checkout에서 canonical external verification helper로 최종 검증을 실행·validate하고 manifest의 source SHA가 전달할 commit과 일치함을 확인한다. 실패하면 local commit을 보존하고 push하지 않는다.
9. Main은 아래 9–12의 push/readback/PR 동작을 main-owned adapter로 구현하고 `scripts/implementation_delivery.py`의 `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`로 실행한다. Callback을 직접 먼저 실행하거나 이전 PASS로 생략하지 않는다. Adapter의 인증·repository/Issue·branch scope 검증은 아래 규칙을 모두 유지한다. Issue worktree는 delivery 전후 모두 detached HEAD로 유지한다. Issue 전용 remote branch가 없으면 detached final commit을 explicit repository URL과 `<final-sha>:refs/heads/<issue-branch>` refspec으로 push해 생성하고, 있으면 현재 작업이 소유한 branch이며 fast-forward 가능한 경우에만 같은 방식으로 갱신한다. `git switch -c`, `git checkout -b`, `git branch` 또는 동등한 명령으로 canonical mirror의 `refs/heads/*`에 구현용 local branch를 만들지 않는다. Default branch에는 직접 push하지 않고 force-push하지 않으며 canonical cache의 mirror remote 설정을 변경하지 않는다.
10. Target repository write 권한이 없으면 같은 push를 반복하지 않는다. Authenticated identity가 fork를 생성할 수 있으면 기존 fork·동명 branch·PR 유무와 fork parent/base SHA를 검증한 뒤 fork의 Issue 전용 branch로 non-force push하고 cross-repository PR을 사용한다. Fork도 쓸 수 없으면 permission blocker와 local commit SHA를 보고한다.
11. Remote readback으로 branch owner, repository, branch SHA가 local commit SHA와 exact-match하는지 확인한다.
12. Default branch를 base로 PR을 생성한다. 제목은 Issue를 식별하고, 본문에는 `Closes #<number>`, 구현 요약, 문서 변경, 실제 검증 명령·결과, 제한사항과 보안상 주의점을 기록한다. PR 생성 자체로 Issue state를 변경하지 않는다.
13. 후속 commit의 push에도 새 head/source/context/result를 결속해 동일 delivery workflow를 사용하며 create_or_reuse_pr는 이미 확인된 같은 PR을 재사용한다. PR 번호를 얻은 뒤 요구사항 문서의 `구현 PR: 미정`을 실제 PR 번호 또는 URL로 교체하고, 문서 검증과 관련 test를 재실행해 별도 commit으로 같은 branch에 non-force push한다.
14. Gitea에서 PR details, base branch, head repository·branch·SHA, state, changed files와 commit을 다시 읽는다. 최종 remote head SHA가 최종 local commit SHA와 일치할 때만 전달 완료로 판정한다.
15. Gitea Actions 또는 commit status가 제공되면 최종 head의 success, failed, pending, run 없음 또는 조회 불가 상태를 읽고 그대로 보고한다.
16. PR 생성 또는 후속 push가 부분 실패하면 blind retry하거나 중복 PR을 만들지 않는다. 이미 생성된 fork·branch·PR과 local commit의 exact 상태 및 안전한 복구 지점을 보고한다.
17. 성공·실패와 관계없이 현재 실행이 만든 임시 server, fixture workspace와 credential helper를 정리하고 부재를 확인한 뒤 보유한 repository lease에 `release_repository`, Issue lease에 `release` request를 보낸다. Persistent Issue worktree는 별도 제거 승인이 없으면 보존한다.
완료 조건: Issue와 source drift, final worktree 상태와 실행 결과가 fresh readback으로 확인되고, Issue worktree가 final commit의 detached HEAD로 남아 있으며, 열린 Gitea PR의 base·head와 remote head SHA가 검증된 최종 local commit에 exact-match하고, mirror fetch가 구현 branch checkout 충돌 없이 성공하며, 임시 resource와 lock 정리가 검증됐다. 열린 PR이 없으면 명시적인 local-only 제한이나 외부 permission blocker가 기록된 경우를 제외하고 스킬은 미완료다.
## 위임 규칙
Main agent는 구현 범위와 최종 결과에 책임을 진다. 독립적인 repository 조사, test 작성, 특정 모듈 구현, 보안 검토 또는 실패 분석은 병렬 위임할 수 있지만 다음을 지킨다.
- Delegate에 Issue body 전체가 아니라 필요한 확정 계약, exact worktree path와 금지된 side effect를 명시한다.
- 같은 파일을 병렬로 수정하는 worker를 만들지 않는다.
- Delegate가 Gitea write, lock 변경, worktree 생성·제거, commit, push 또는 reset을 수행하지 못하게 한다.
- Delegate의 성공 주장을 evidence로 간주하지 않고 main agent가 diff와 test output을 직접 확인한다.
- 외부 side effect는 main agent만 수행하고 즉시 authoritative readback한다.
## 주의점과 금지 사항
- 미해결 질문이나 충돌하는 결정을 임의로 선택하지 않는다.
- Issue body에 없는 기능, migration, refactor 또는 dependency를 편의상 추가하지 않는다.
- Moving branch나 stale cache를 구현 근거로 사용하지 않는다.
- Bare mirror에 구현용 local branch를 만들지 않는다.
- 다른 worktree의 변경을 가져오거나 정리하지 않는다.
- 기존 dirty state를 reset, clean, stash, checkout 또는 force remove하지 않는다.
- test를 통과시키기 위해 production security gate, validation 또는 실패 경로를 약화하지 않는다.
- 실제 credential, 운영 DB 또는 사용자 금융 데이터를 test나 artifact에 사용하지 않는다.
- 실행하지 않은 test, 읽지 않은 파일 또는 확인하지 않은 Gitea 상태를 성공 evidence로 보고하지 않는다.
- Issue 구현 요청 범위를 벗어난 commit·push·PR 변경, force-push, merge, Issue 변경, release 또는 deployment를 수행하지 않는다.
- 실패하거나 불확실한 외부 write를 blind retry하지 않는다.
## 검증 및 보고
완료 보고에는 다음을 구분해 포함한다.
- authenticated Gitea identity와 exact target
- approval snapshot의 Issue `updated_at`, body digest와 미해결 질문 검증
- cache path, bare·remote 검증, fetch 결과와 Gitea/local SHA
- Discovery pinned SHA, selected base SHA와 차이 판정
- worktree path, 신규 생성 또는 재사용, detached·locked 상태와 final HEAD
- 요구 문서별 조치, exact path와 canonical template 검증
- acceptance criterion별 구현 위치와 test evidence
- baseline, targeted와 full-suite 명령의 실제 exit 결과
- 최종 보고와 PR에 재사용한 단일 검증 기록의 exact command, exit code와 pass·skip·fail 수치
- modified file 전체와 범위 내 변경 근거
- staged, unstaged, untracked, 미완료 Git operation, local commit과 remote branch SHA
- Issue와 default branch의 최종 drift readback
- PR 번호·URL·base·head·state와 PR head SHA readback
- 최종 head의 Actions·commit status와 실제 runtime 검증을 대체한 경우의 미검증 범위
- 임시 process·workspace·credential helper 정리와 lock 해제 확인
- 수행한 commit·push·PR write와 수행하지 않은 merge·Issue·release write
- blocker, 기존 실패, unresolved risk 또는 partial 상태
파일 작성, process exit, test 일부 통과, local commit 또는 delegate 보고만으로 완료를 주장하지 않는다. 모든 승인된 산출물이 worktree에 존재하고 acceptance matrix, 실제 test output, final diff, remote branch와 열린 PR의 fresh Gitea readback이 일치할 때만 구현 완료로 판정한다. Exact Issue 구현 요청에서 PR 생성은 별도 승인 대상이 아니라 완료 자체의 일부다.
## Canonical external verification evidence
검증 실행 기록은 공통 [Repository cache 운영 지침](../../../../../operations/repository-cache.md)의 정본 경로 `<harness-root>/repositories/<owner>/<repo>/worktrees/issue-<number>/verification/<run-id>`에 보존한다. Product `checkout`과 artifact root를 분리하며, 회차별 evidence를 제품 `docs/verification/`에 새로 커밋하지 않는다. 기존 정본 문서·테스트·fixture·추적 파일은 자동 이동·삭제·ignore하지 않는다.
인증된 Issue/cache/worktree admission을 마친 caller가 broker Issue lease 아래 evidence `run` 전에 live lease의 exact harness root·Issue identity를 인증하고 root를 준비한다. Stage별 새 run-id를 사용한다. 아래 prepare는 `verification/`과 `<run-id>/`를 각각 별도 생성하고 각 directory를 실행 UID 소유 mode `0700`으로 만들며 즉시 `lstat`한다. Symlink, owner·mode 불일치와 collision은 거부하고 기존 malformed root를 자동 chmod하거나 repair하지 않는다. 이 경로에는 중간 parent가 umask를 따르는 `Path.mkdir(parents=True, mode=0o700)`를 사용하지 않는다. Helper는 lease/cache를 획득·복구하지 않는다. `<harness-root>`는 실제로 읽은 contract path로 고정하고 다음 변수는 그 root와 인증된 Issue/run identity에서 결정한다. 아래 명령은 harness root에서 호출한다.
```sh
python3 harness/lib/verification_root.py prepare \
--root "$HARNESS_ROOT" --owner "$OWNER" --repository "$REPOSITORY" \
--issue "$ISSUE" --run-id "$RUN_ID" --handle "$HANDLE"
python3 harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py run \
--contract-path "$HARNESS_ROOT/harness/operations/repository-cache.md" \
--repo-root "$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/checkout" \
--artifact-root "$VERIFICATION_RUN_ROOT" \
--plan evidence-plan.json --output evidence-manifest.json
python3 harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py validate \
--contract-path "$HARNESS_ROOT/harness/operations/repository-cache.md" \
--repo-root "$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/checkout" \
--artifact-root "$VERIFICATION_RUN_ROOT" \
--manifest evidence-manifest.json
```
`VERIFICATION_RUN_ROOT`는 `$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/verification/$RUN_ID`의 exact 절대 경로다. Plan의 `hash_paths`만 repository-relative이고 plan/output/manifest는 run-relative다. Source filename과 artifact filename이 같아도 같은 파일로 취급하지 않는다. `run`은 **strict clean checkout**, evidence **exclusion 없음**, full SHA·source/plan digest와 실제 command/exit code/bounded output binding을 요구하고 `evidence-manifest/v3`를 게시한다. Existing manifest를 덮어쓰지 않는다. `validate`도 같은 roots와 exact current HEAD를 요구한다. Dirty 개발 tree에서 validation 성공을 주장하지 않으며, final tracked 산출물을 commit한 뒤 그 exact commit을 검증하고 실패하면 push하지 않는다.
PR/결과 보고에는 stage/run-id, source SHA, manifest·plan digest, 실제 command/result/limitations와 reviewer 접근 수단을 함께 기록한다. Same-UID reviewer는 기록만 read-only로 읽고, distinct-UID/host이면 승인된 credential-free read-only export와 digest/readback receipt를 제공한다. 경로만 제시하거나 권한을 완화하는 것은 접근 검증이 아니다. 확인할 수 없으면 `evidence_gap`으로 보고한다. Product 테스트 증거와 harness 자체 검증을 구분하고, 인증정보 값은 `[REDACTED]`로만 기록한다.
Remote branch/PR head를 authoritative하게 읽어 source SHA·exact artifact와 결합하고 cleanup 전 실제 record readback을 확인한다. Helper의 local validate가 Gitea readback을 대신하지 않는다. Checkout 제거와 verification 폐기는 분리하며 완료 기록은 보존한다. 기존 v1은 `validate-legacy`의 read-only 호환 경로만 사용하고 새 publication proof로 사용하지 않는다.
Command deadline 계약: 새 plan의 각 command는 `timeout_seconds`를 명시한다. Boolean·문자열·누락·비유한 값·0 이하·3600 초 초과를 실행 전에 거절하며 암묵적인 무제한 실행을 허용하지 않는다. Monotonic wall-clock deadline은 무출력 대기, stdout EOF 이후 실행, leader 종료 뒤 descendant가 유지한 pipe까지 적용한다. 각 command는 별도 session/process group에서 실행하고 timeout·출력 초과·예외 시 해당 process group을 TERM → 제한된 grace → KILL로 정리하고 direct child를 bounded wait로 회수한다. Leader가 종료됐어도 같은 group의 살아 있는 descendant를 남기지 않는다. 이것은 sandbox가 아니며 별도 session으로 이탈한 descendant의 containment나 grandchild 직접 회수를 보장하지 않는다.
`evidence-manifest/v3`는 실제 timeout, `execution_status`, `termination`의 `term_sent`·`kill_sent`·`direct_child_reaped`·`group_cleanup_completed` boolean 기록과 안전한 bounded 부분 출력을 결합한다. 새 claim pointer는 기존 6개에 timeout·실행 상태·종료 기록을 추가한 9개다. Linux `/proc`로 같은 group의 non-zombie 프로세스를 확인하며 확인·정리에 실패하면 `cleanup_failed`로 차단한다. TERM grace는 0.25 초, KILL 후 group 종료·direct-child 회수 예산은 0.5 초이며 오류 fallback wait도 0.1 초로 제한한다. 새 command의 `max_output_bytes`는 0–1048576 범위의 non-boolean 정수만 허용한다. `execution_status`는 `completed|timeout|output_limit|cleanup_failed|descendant_cleanup`이다. 정상 종료 leader 뒤의 살아 있는 descendant를 신호로 정리한 경우도 `descendant_cleanup` 실패이며, `completed`에는 TERM/KILL 신호가 있을 수 없다. Timeout·출력 한도 초과·정리 실패는 expected exit code와 같아도 성공이 아니며 **TDD RED** 또는 제품 assertion 실패로 분류하지 않는다. 해당 실행은 실패로 보존하고 뒤의 command를 실행하지 않으며, 실행된 prefix의 claim만 기록한다. Partial output에도 기존 secret 탐지를 적용하고 인증정보 유출은 fail-closed한다. 자동 재실행은 하지 않으며 새로 승인된 실행에는 새 run-id를 사용한다.
기존 `evidence-manifest/v2`는 명시적 `validate-archived-v2 --contract-path <canonical-contract> --repo-root <checkout> --artifact-root <canonical-run> --manifest <run-relative-manifest>`로 read-only 호환 검증한다. Canonical identity, exact current HEAD, source/plan/output binding을 유지하지만 과거 기록에 없던 timeout이나 종료 보장을 추론하지 않는다. 새 `run`/`validate`에는 v2 fallback이 없다. v1은 기존 `validate-legacy`만 사용한다. 두 archive 경로의 성공은 새 실행·TDD RED·fresh publication proof가 아니며 기존 artifact를 수정하거나 새 schema로 자동 변환하지 않는다.
@@ -127,3 +127,100 @@ def reconcile_implementation(adapter: Any, *, head: str, number: int,
if verified is not True:
_reject("reconciliation intent or protected state mismatch")
return {"remote_head": remote_head, "pr": bounded_pr}
def deliver_approved_implementation(adapter: Any, *, head: str, declaration: Any,
push_intent: Any, push_approval: Any,
pr_intent: Any, pr_approval: Any,
coordinator: Any, on_attempt: Any) -> dict[str, Any]:
"""Compose two approved operations; callbacks own scope/lease/provider proof.
Legacy helpers remain stage-only. This path is not a transport sandbox or a
durable transaction. on_attempt must archive provenance before external write.
"""
import sys
root = str(Path(__file__).resolve().parents[6])
if root not in sys.path:
sys.path.insert(0, root)
from harness.lib.impl_operation import Adapter, Coordinator, validate
_validate_head(head)
for name in ("read_remote_head", "read_pr"):
if not callable(getattr(adapter, name, None)):
_reject("trusted operation readback adapter required")
if not isinstance(coordinator, Coordinator) or not callable(on_attempt):
_reject("caller-owned coordinator and attempt recorder required")
for name in ("preflight_operation", "verify_operation", "find_intended_pr", "push_operation", "create_or_reuse_pr_operation"):
if not callable(getattr(adapter, name, None)):
_reject("trusted operation adapter required")
try:
validate(push_intent, push_approval)
validate(pr_intent, pr_approval)
except (ValueError, TypeError):
_reject("invalid approved operation")
target = (push_intent.target, push_intent.target_id, push_intent.source_sha)
if push_intent.operation != "push" or pr_intent.operation != "pr.create":
_reject("only push and pr.create intents are supported")
if push_intent.source_sha != head or (pr_intent.target, pr_intent.target_id, pr_intent.source_sha) != target:
_reject("operation target/source mismatch")
if type(push_intent.payload) is not dict or type(pr_intent.payload) is not dict:
_reject("explicit operation payloads required")
if push_intent.payload.get("head") != head or pr_intent.payload.get("head") != head:
_reject("payload head differs from approved source")
if type(pr_intent.payload.get("body")) is not str or not pr_intent.payload["body"]:
_reject("explicit intended PR body required")
def stage(intent, approval, mutation, readback, expected):
entered = False
def preflight(item):
_delivery_adoption.require_adoption(declaration,
expected_source={"kind": "git_tree", "revision": head},
expected_stage="implementation-delivery")
return adapter.preflight_operation(item)
def mutate(item):
nonlocal entered
_delivery_adoption.require_adoption(declaration,
expected_source={"kind": "git_tree", "revision": head},
expected_stage="implementation-delivery")
entered = True
return mutation(item)
def verify(item, before, observed):
# A failed local approval gate is not an uncertain remote mutation.
return entered and expected(observed) and adapter.verify_operation(item, before, observed) is True
return coordinator.execute(intent, approval,
Adapter(preflight, True, mutate, readback, verify), on_attempt)
push = stage(push_intent, push_approval, lambda item: adapter.push_operation(item),
lambda item: adapter.read_remote_head(),
lambda value: type(value) is str and value == head)
if push.status != "verified":
return {"status": "partial", "push": push, "pr": None, "artifact": None}
intended_number = None
observed_pr = None
def create_pr(item):
nonlocal intended_number
intended_number = _validate_pr_number(adapter.create_or_reuse_pr_operation(item))
return {"number": intended_number}
def pr_readback(item):
nonlocal intended_number, observed_pr
if intended_number is None:
# The callback must return exactly one intended PR, or reject. GET only.
found = adapter.find_intended_pr(item)
if type(found) is not dict:
_reject("unique intended PR unavailable")
intended_number = _validate_pr_number(found.get("number"))
observed_pr = adapter.read_pr(intended_number)
return observed_pr
def pr_ok(value):
try:
_validate_readback(value, _validate_pr_number(intended_number), head)
except AdoptionError:
return False
return True
pr = stage(pr_intent, pr_approval, create_pr, pr_readback, pr_ok)
if pr.status != "verified":
return {"status": "partial", "push": push, "pr": pr, "artifact": None}
# Use the validated readback, not private adapter state or an unverified read.
return {"status": "verified", "push": push, "pr": pr,
"artifact": {"remote_head": head,
"pr": _validate_readback(observed_pr, _validate_pr_number(intended_number), head)}}
@@ -1,7 +1,7 @@
---
name: gitea-issue-rework
description: Gitea PR 검토 지적을 계약에 맞춰 재작업하고 검증한다.
version: 0.2.17
version: 0.3.0
author: genichin, Hermes Agent
license: MIT
platforms: [linux]
@@ -13,387 +13,37 @@ metadata:
# Gitea Issue Rework
`gitea-issue-implementation`으로 전달한 기존 Gitea PR에 대해 `gitea-spec-agent` 또는 승인된 `gitea-review-agent`가 남긴 검토 결과를 exact reviewed head와 승인된 Issue 계약에 결합해 재작업한다. 유효한 지적만 source·문서·test·evidence에 반영하고 같은 Issue 전용 branch에 non-force push한 뒤, exact final head에 결합된 bounded PR comment로 재검토 handoff를 게시한다. 새 요구사항을 만들거나 명세를 구현에 맞춰 완화하지 않으며 merge, Issue 변경과 review 작성은 수행하지 않는다.
기존 PR의 검토 finding을 승인 계약에 맞춰 재작업하고 exact-head handoff를 한다. Main owns interpretation, approval, credentials, Git/Gitea mutation, integration and delivery; helpers neither grant authority nor prove end-to-end completion. Follow [Impl operation architecture](../../ARCHITECTURE.md); detailed preserved workflow and gates are in [workflow reference](references/workflow.md).
Gitea 인증과 authoritative readback은 [Gitea authenticated access 운영 지침](../../../../operations/gitea-access.md), cache·lock·worktree 규칙은 [Repository cache 운영 지침](../../../../operations/repository-cache.md), 문서 형식은 `docs/` 아래 각 문서 유형의 README와 TEMPLATE만 정본으로 사용한다.
## When to use
A REQUEST_CHANGES/actionable finding on an existing implementation PR; do not use for initial delivery. Never infer unresolved decisions or authorize merge, force-push, Issue changes, release, or deployment.
## Test inventory source-binding gate
## Ordered lifecycle
Baseline/current inventory를 생성·전달·채택할 때 `check-tests 이전`에 `harness/lib/test_inventory_binding.py`를 실행한다. Inventory의 자기 선언을 신뢰하지 않고 독립적으로 고정한 revision과 stage를 expected 입력으로 제공한다. Exact Git blob의 파일 존재·SHA-256 및 collection scope 결속을 [공통 source-binding 정본](../../../../operations/repository-cache.md#test-inventory-git-source-binding-preflight)에 따라 확인하고, 통과한 ID/source가 이후 구조 guard 입력과 같은지도 대조한다. 불일치 증빙을 채택하거나 현재 collection을 baseline으로 재명명하지 않는다. PASS가 named test의 존재·collection 실행·의미 보존을 인증하지는 않으며 main의 실제 실행 provenance 검토와 기존 revision evidence 영향 평가를 유지한다. 이 gate는 제품 실행·별도 checkout 권한이나 자동 제품 변경 요청을 추가하지 않는다.
1. **Snapshot과 finding 판정.** Current Issue/PR/review identity, contract digest, reviewed/current head, complete collections와 immutable snapshot을 읽는다. Input gate·delegation/source-binding·runtime adoption을 통과하고 blocked/ambiguous면 중단한다.
2. **Admission·계획.** Broker/cache/worktree/evidence admission, test inventory binding, finding/acceptance matrix, exact allowlist와 baseline을 고정한다. 기존 state를 복구 목적으로 reset하지 않는다.
3. **Test-first 재작업.** 유효 finding만 승인 계약에 따라 고치고 evidence gap은 주장 범위에 맞는 증거로 보완한다. Existing behavior/security gates 약화 금지.
4. **검증·검토·commit.** Focused/full/runtime/browser tests, exact-source evidence, diff/inventory checks와 independent review 실행. Drift 확인 후 exact stage·commit·strict verification.
5. **별도 delivery operations.** Push, PR-body update, canonical PR-top-level comment create/reuse는 각각 승인·preflight·단일 mutation·fresh readback·operation-specific verification을 가진다. Main-owned integration paths only; helpers are not sandbox/authority or end-to-end certificates.
6. **종료·handoff.** 불확실한 acknowledgement는 read-only reconcile; 자동 재시도 금지. Resource/lease 정리, verified receipt 및 후속 reviewer 필요를 보고한다.
## Delegation evidence acceptance gate
## Shared mandatory gates
위임 결과 채택 이전에 `harness/lib/delegation_evidence.py`를 [공통 증빙 채택 정본](../../../../operations/repository-cache.md#delegation-evidence-acceptance-preflight)에 따라 실행한다. main이 독립적으로 고정한 context에 source/bundle digest, 허용 작업, 검증 항목과 독립 실행 catalog를 넣으며 worker 자기 선언으로 context를 만들지 않는다. Source line/named test anchor와 실제 보존된 회귀 테스트·RED/GREEN 기록을 대조하고, bundle 추출 범위 밖 부재 주장은 authoritative Git 조회 없이 채택하지 않는다. Import·환경 오류를 RED로 취급하거나 확인되지 않은 위임 주장을 최종 판정·delivery·publication 근거로 옮기지 않는다. Static-only worker가 기존 독립 실행 근거를 인용하는 것과 직접 실행하는 것을 구분한다. BLOCK/입력 거부이면 해당 증빙 채택을 중단하고 main이 실제 기록과 의미를 재검증한다. Gate는 sandbox, 실행 인증 또는 assertion 의미 판정기가 아니며 기존 compare-source/check-tests와 role의 제품 실행 제한을 대체하지 않는다.
- Apply test inventory source-binding before check-tests; inventory PASS does not prove test existence, collection, execution, or semantic preservation.
- Before accepting delegated evidence run `harness/lib/delegation_evidence.py` under its canonical contract and independently verify actual artifacts. At each consumer call `require_adoption`; declare `mode=none` or exact `mode=delegated`, never rely on missing declarations or stale PASS receipts. This is not a sandbox.
- Preserve canonical input gates, complete pagination, identity/source binding, cache-mode restrictions, broker leases, worktree admission, exact-source evidence and cleanup. Read the canonical repository-cache and Gitea access operations each run.
- Operation helpers are stage integrations, not authority grants, trusted-adapter substitutes, or completion certificates. The main-owned trusted adapter performs preflight, one mutation and authoritative readback; see the operation integration contract before invoking it.
실제 소비 함수에서도 [runtime consumption 정본](../../../../operations/repository-cache.md#delegation-adoption-runtime-consumption)의 `require_adoption`을 통과한다. main은 위임 근거 비사용을 `{"mode":"none"}`, 사용을 `mode=delegated` exact 선언으로 명시하며 누락이나 이전 PASS receipt를 허용하지 않는다. 판정/render, delivery artifact, native publication마다 새 검증을 수행하고 소비 source/stage에 결속한다. 최초 구현 delivery는 `gitea-issue-implementation/scripts/implementation_delivery.py`의 `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`에서 main-owned push/PR adapter를 호출한다. Adapter는 push/read_remote_head/create_or_reuse_pr/read_pr를 제공하고 기존 인증·repository/Issue scope·non-force explicit refspec·remote readback 계약을 지킨다. 새 진입점을 쓰지 않는 직접 push/PR 생성은 이 workflow를 우회하는 것으로 금지한다. Gate 밖 직접 Git/API 실행을 sandbox로 차단한다고 주장하지 않는다. 기존 semantic/source/identity/lease 검증과 외부 readback은 그대로 유지한다.
## References
## Managed-lock broker gate
- [Full workflow, preserved prose and detailed gates](references/workflow.md)
- [Main impl architecture](../../ARCHITECTURE.md)
Bootstrap 이후 이 Issue 재작업도 canonical [Repository cache 운영 지침](../../../../operations/repository-cache.md)의 공통 managed-lock broker 하나만 사용한다. `serve`에 canonical Issue identity, role, workflow run-id와 TTL을 전달해 broker-generated runtime을 만들고 ready JSON의 `handle`/`socket` 경로만 사용한다. 경로는 `<issue>/runtime/<workflow-run-id>/broker.handle`과 `broker.sock`이며 verification run-id와 독립적이다. Runtime 생성은 approval이나 worktree admission을 대신하지 않는다. Cache bootstrap 이후 broker가 Issue lock 아래 runtime을 준비하고, ready 이후 각 workflow의 진입 검증·승인 조건을 확인한다. 같은 handle로만 public `request` API의 `acquire_repository`/`release_repository`를 요청하고 TTL 전에 `renew`한다. Client는 raw token을 inspect/log하지 않으며 broker request API가 token을 캡슐화한다. 정상·오류·cancel cleanup은 보유한 repository lease release, Issue `release`, broker wait/reap, `probe`의 `free=true` 확인까지 동일하며 그 전에는 completed/log/report를 주장하지 않는다. Raw `flock ... sleep`, lock-file unlink와 client-side lock holder는 금지한다.
## Operation integration
## 사용 시점
Before delivery, read [operation API contract](../../OPERATIONS.md). Use the coordinator-backed entrypoints; a legacy stage receipt is not end-to-end authority.
다음처럼 exact repository, source Issue, 기존 구현 PR과 재작업할 review를 식별할 수 있는 요청에 사용한다.
## Required workflow contract
```text
<owner>/<repository> Issue #<issue-number> 구현 PR #<pull-number>의 검토 지적을 재작업하라
```
이 요청은 아래 작업만 승인한다.
- 기존 canonical Issue worktree 검증과 재사용
- 승인된 계약 안에서 검토 지적을 해결하는 문서, source, test, 설정과 evidence 수정
- synthetic fixture와 test-only credential을 사용한 build, test, lint, 실제 runtime·browser 검증
- 기존 Issue 전용 branch 위의 non-amended commit 생성과 non-force push
- 현재 PR 본문의 기존 의미와 `Closes #<issue-number>`를 보존하면서 재작업 요약과 최종 검증 결과를 갱신하는 작업
- 이 스킬의 canonical 형식과 deduplication 계약을 만족하는 재작업 결과 PR comment 하나의 생성 또는 기존 exact comment 재사용
다음 작업은 별도 요청이 없으면 승인되지 않는다.
- 새 worktree나 대체 PR 생성, 다른 branch 또는 default branch로 push
- force-push, rebase, amend, reset, clean, stash 또는 history rewrite
- PR merge·close·reopen, native review·review reply·임의 comment 작성, reviewer 지정 또는 approval
- Source Issue `#<issue-number>`의 body·comment collection·title·state·label·assignee·milestone 변경. Canonical PR comment는 exact Pull Request `#<pull-number>`의 top-level conversation에만 쓰며, provider가 이를 PR 자체의 underlying issue record `#<pull-number>` comment로 저장하는 projection은 허용한다. Source Issue record와 PR underlying issue record를 identifier·object kind·pull relation으로 구분할 수 없으면 게시하지 않는다.
- 승인된 Discovery의 범위·결정·acceptance criterion 변경
- release, package 또는 deployment 게시
최초 구현, 독립 review, Discovery 변경 또는 이미 반영된 지적의 재검토에는 사용하지 않는다.
## 입력 승인 gate
초기 review decision gate는 [references/input-gate.md](references/input-gate.md)의 public `input-gate`를 **no_op 또는 source edit 전에 반드시 먼저** 호출한다. 이 gate는 선택된 source review나 renderer `expected.json`을 입력으로 요구하지 않는다. Receipt의 `rework` ID를 읽은 뒤 finding은 수동으로 검증하며 automatic `COMMENT` classification은 지원하지 않는다. Gate의 `no_op`은 구현 재작업을 건너뛴다는 뜻일 뿐 native review publisher의 idempotent review reuse/no-op과 다르다. Native publication, journals, lease/worktree admission, full Discovery approval semantics와 pre-write freshness는 이 receipt로 증명되지 않으며 독립적으로 수행한다.
Repository file을 수정하기 전에 다음을 모두 검증한다.
- `owner`, `repository`, Issue 번호와 PR 번호가 canonical identifier다.
- Impl role의 authenticated stable account identity와 credential provenance를 확인했다.
- PR은 open이고 source Issue와 exact `Closes #<issue-number>` 하나로 결합되며 PR author의 stable account ID가 현재 authenticated impl role의 stable account ID와 exact-match하는 기존 구현 PR이다.
- PR base는 target repository의 default branch이고 head repository·branch·40자리 full SHA를 식별할 수 있다.
- Current Issue body는 유효한 `gitea-agent:discovery-draft/v2` 계약이고 미해결 질문이 없으며 body SHA-256을 계산할 수 있다.
- 재작업 대상 native review의 ID, author stable account ID, state, body, reviewed commit SHA, stale·dismissed 여부와 inline comments를 complete pagination으로 읽었다.
- Review author가 허용된 spec 또는 code-review role이고 impl role과 다른 identity다.
- `REQUEST_CHANGES` 또는 명시적인 actionable finding이 있으며 각 finding의 계약 근거와 대상 경로를 판독할 수 있다.
- Review가 참조한 contract digest가 있으면 current Issue body digest와 exact-match한다.
- Reviewed head가 current PR head와 exact-match한다. 다르면 current head에 결합된 더 최신 authoritative review를 사용하고, 없으면 이전 지적을 새 head에 임의 적용하지 않고 중단한다.
- Changed-file, review, review-comment와 commit pagination이 완전하고 diff가 truncated·ambiguous하지 않다.
- Canonical Issue worktree와 PR head branch를 현재 실행이 안전하게 재사용할 수 있다.
Canonical input-gate의 `decision.rework_review_ids`는 현재 head·계약에 결합된 role별 최신 `REQUEST_CHANGES`만 포함한다. Spec 또는 code-review role 중 하나 이상에 유효한 최신 수정 요청이 있고 다른 차단 사유가 없으면 `rework`다. 다른 role의 유효 review 부재는 `missing_current_review`로 기록하되 이 수정 요청의 재작업 착수를 차단하지 않는다. 수정 요청 없이 review가 부족하면 `blocked`이며 no-op으로 종료하지 않는다. `rework`에서도 finding의 계약 근거·scope·독립 승인과 lease/worktree 조건을 별도로 확인한 뒤에만 수정한다. `COMMENT`는 자동 actionable 판정을 하지 않고 `blocked`로 남겨 main의 수동 분류 또는 별도 승인 절차로 넘긴다. 특히 `blocked_spec`, 계약 모순, 선택되지 않은 설계 선택 또는 source reality와 계약 충돌은 구현 재작업으로 해결하지 않으며 Discovery 재검토가 필요하다고 보고한다. `blocked` receipt로 source를 수정하거나 no-op을 주장하지 않는다. Current head·계약의 spec와 code-review role별 최신 유효 review가 모두 `APPROVED`인 fresh `no_op` receipt에서만 write 없는 종료를 한다. 다른 role의 승인·old-head/stale/dismissed review·본문의 해결 주장은 이 조건을 대체하지 않는다.
## Snapshot과 finding matrix
첫 write 전에 canonical collection의 test inventory와 영향을 받는 API·CLI·필드·상태·cleanup 의미를 snapshot/finding matrix에 연결하고 [공통 workflow guard 계약](../../../../operations/repository-cache.md#shared-workflow-guard-gates)을 적용한다. `python3 <harness-root>/harness/lib/workflow_guard.py check-tests --input JSON`과 `check-contracts --input JSON`을 결과 채택 전에 실행한다. RED/GREEN records must be named: 각 유효한 finding은 current test ID와 실제 실행 기록에 결합하며 환경/import 오류나 반증된 지적에 가짜 RED를 만들지 않는다. 읽기 전용 검토에는 위임 전 source snapshot과 채택 전 비교를 요구하고, 승인된 쓰기 worker의 편집에는 diff·의미 검토를 적용한다. 기록은 제품 checkout 밖 정본 verification run에 보존하며 helper schema·증거를 검증한다. gate failure는 운영 근거 무효이지 자동 제품 결함이 아니다.
첫 write 전에 다음 immutable snapshot을 보존한다.
- Issue title, body, state, `updated_at`, labels, assignees, milestone, 전체 comments와 body SHA-256
- PR title, body, state, `updated_at`, base·head repository와 branch, base·head SHA, author, changed files, commits, status, workflow run, PR top-level comment collection의 exact cardinality와 provider가 노출하는 comment-count field baseline
- 전체 native review baseline과 대상 review의 ID, author, state, body digest, reviewed head, submitted time, stale·dismissed 상태와 inline comments
- Worktree state identity, base SHA, current HEAD, detached·locked 상태, staged·unstaged·untracked file과 미완료 Git operation
기존 canonical requirement revision을 갱신하거나 기존 계약 보존이 승인 조건이면 pinned source revision 또는 직전 승인 revision의 모든 requirement·acceptance criterion·상태 불변조건을 의미 단위로 추출해 다음 Contract Preservation Inventory를 같은 snapshot에 고정한다. ID 변경만으로 보존을 판정하지 않는다.
```text
baseline_revision: <full SHA>
path: <repository-relative canonical requirement path>
baseline_id: <기존 requirement 또는 AC ID>
semantic_key: <ID와 독립적인 안정된 의미 식별자>
meaning: <정상·실패·경계·불변조건의 bounded 설명>
reviewed_ids: [<reviewed head의 대응 ID>...]
reviewed_status: preserved | intentionally_changed | missing | ambiguous
planned_ids: [<재작업 후 대응 ID>...]
planned_status: preserved | intentionally_changed | missing | ambiguous
authorization: Issue contract citation authorizing intentional change, or none
evidence_paths: [<source/test/document path>...]
verification: <test/static/runtime/browser evidence>
```
Baseline 항목은 중복 없이 정확히 한 번 나타나야 한다. `intentionally_changed`에는 승인된 Issue 근거가 필요하다. `reviewed_status`의 `missing`은 finding matrix에 포함하고 `ambiguous`는 계약 판단 불가로 중단한다. Repository 첫 write 전 계획 기준 `planned_status`가 `missing=0`, `ambiguous=0`이어야 한다. Contract Preservation Inventory를 delegate할 때는 immutable pre-write snapshot에 대해 완료·회수한 뒤에만 mutating worker를 시작하며, 편집과 병렬 실행해 중간 상태를 판정하지 않는다.
모든 finding을 다음 matrix의 정확히 한 항목으로 분류한다.
```text
finding: <review-id>/<stable finding id>
reviewed_head: <full SHA>
contract: <Issue/requirement/acceptance criterion 근거>
classification: valid_fix | evidence_gap | reviewer_misunderstanding | blocked_spec
implementation_paths: [<repository-relative path>...]
test_paths: [<repository-relative path>...]
verification: <실행할 command/runtime/browser evidence>
result: pending | resolved | unresolved
reason: <bounded 판정 근거>
```
- `valid_fix`: 승인 계약을 위반한 source·문서·설정·test 결함이다.
- `evidence_gap`: 구현 적합성은 확인 가능하지만 exact-head-bound 실행·runtime·browser·CI evidence가 부족하다.
- `reviewer_misunderstanding`: 지적이 승인 계약 밖의 새 요구사항이거나 authoritative tree와 맞지 않는다. 이를 만족시키려고 범위를 확장하지 않고 근거와 함께 unresolved로 보고한다.
- `blocked_spec`: 명세 모순이나 새 결정이 필요하다. 모든 repository write를 중단한다.
완료 조건: 모든 review finding과 unsatisfied·indeterminate criterion이 matrix에 정확히 한 번 나타나고, `valid_fix`와 `evidence_gap`마다 변경 또는 검증 경로가 있으며, 이미 satisfied인 계약을 약화하는 항목이 없다. Contract Preservation Inventory가 필요한 경우 baseline 전체가 대응되고 `missing=0`, `ambiguous=0`이다.
## Canonical 재작업 결과 comment
재작업 branch를 전달한 뒤 reviewer가 새 head를 자동으로 재검토할 수 있도록 exact PR의 top-level conversation에 comment 하나를 게시한다. Adapter target은 object kind가 pull request이고 index가 exact `<pull-number>`인 record여야 한다. Provider가 PR comment를 underlying issue comment로 저장하면 exact comment endpoint target의 issue index가 `<pull-number>`이고 fresh PR metadata가 그 record를 같은 pull request로 식별함을 증명해야 한다. Comment object가 target kind를 반복하지 않는 API에서는 검증된 endpoint target과 PR metadata binding을 사용한다. Source Issue `<issue-number>`, inline comment, review reply 또는 native review로 대체하지 않는다. Marker를 제외한 body를 먼저 canonical UTF-8/LF bytes로 직렬화하고 그 SHA-256을 `result-sha256`으로 계산한다.
Canonical body와 marker는 `scripts/rework_comment.py render`로만 생성하고, 게시 또는 exact comment 재사용 뒤에는 authoritative normalized readback을 `scripts/rework_comment.py verify`로 검증한다. `status=verified`와 `readback_verified=true`인 `gitea-rework-handoff/v1` receipt가 없으면 완료 조건을 충족하지 않으며 PR body update나 write 성공 response는 receipt를 대체하지 않는다.
```markdown
## 구현 재작업 결과
- Source Issue: <issue-number>
- Pull Request: <pull-number>
- Source Review: <review-id>
- Reviewed head: `<reviewed-head-sha>`
- Rework head: `<final-head-sha>`
- Contract SHA-256: `<contract-sha256>`
- Result: `rework_delivered|partial`
### 지적 처리
- `<finding-id>` — `resolved|unresolved`
- 분류: `valid_fix|evidence_gap|reviewer_misunderstanding`
- 변경: <repository-relative paths 또는 없음>
- 검증: <bounded command, runtime, browser 또는 정적 evidence>
- 근거: <bounded reason>
### 최종 검증
- Local checks: <exact command와 exit code 및 pass·skip·fail 요약>
- Runtime/browser: <실행 결과 또는 해당 없음·미검증>
- Commit status: <success|failure|pending|없음|조회 불가>
- Actions runs: <success|failure|pending|없음|조회 불가>
### 다음 단계
Reviewer가 rework head를 새 snapshot으로 다시 검토해야 하며 이 comment는 기존 `REQUEST_CHANGES`를 해제하거나 승인을 의미하지 않는다.
<!-- gitea-agent:implementation-rework/v1 owner=<owner> repository=<repository> issue=<issue-number> pr=<pull-number> review=<review-id> reviewed-head=<reviewed-head-sha> head=<final-head-sha> contract-sha256=<contract-sha256> result-sha256=<result-sha256> correlation=<correlation> -->
```
- `rework_delivered`는 모든 `valid_fix`와 `evidence_gap`이 `resolved`이고 final branch delivery와 검증이 완료된 경우에만 사용한다. 그 외 전달 가능한 결과는 `partial`이며 unresolved 이유를 숨기지 않는다.
- Finding은 matrix 순서로 정확히 한 번 렌더링한다. 이미 satisfied였고 재작업하지 않은 criterion을 반복해 comment를 부풀리지 않는다.
- Path, SHA, enum, command와 수치는 검증된 값을 원문 그대로 사용한다. Human-readable text에는 credential, token, 실제 secret, mention, 외부 지시, HTML 또는 실행 가능한 payload를 포함하지 않는다.
- 모든 human-readable comment line에서 local 또는 cross-repository hash-number 자동참조 문법을 쓰지 않는다. Escape한 표기와 Markdown/code 안의 표기도 금지한다. Source Issue, Pull Request, Source Review 예시는 위처럼 plain decimal number로 쓴다. Hidden canonical marker의 identifier와 correlation 입력은 이 표시 규칙 때문에 바꾸지 않는다. Marker를 제외한 exact UTF-8/LF body에 대해 `result-sha256`을 다시 계산한다. 기존 PR body의 의도된 `Closes #<issue-number>`는 그대로 보존하며 변경하지 않는다.
- Marker-free body는 LF로 정확히 하나 끝나야 한다. 게시 body는 marker-free bytes, marker line과 최종 LF 하나를 순서대로 사용하며 trailing whitespace를 금지한다.
- `correlation`은 `owner`, `repository`, base-10 Issue 번호, base-10 PR 번호, base-10 review ID, lowercase reviewed head, lowercase final head와 lowercase contract digest를 이 순서로 NUL 하나씩 구분한 UTF-8 bytes의 SHA-256이다. `result-sha256`은 correlation 입력에 넣지 않는다.
- 게시 전 exact PR top-level comment endpoint target과 fresh PR metadata binding을 검증하고 그 scoped collection만 complete pagination한 뒤 같은 correlation을 찾는다. Source Issue comment endpoint, inline-comment·review-reply endpoint와 native review collection은 검색 집합에서 제외하며 endpoint scope와 PR relation을 구분할 수 없으면 fail-closed한다. 0개면 exact body를 한 번 생성하고, 1개이며 author stable account ID·body·marker·digest가 모두 같으면 write 없는 no-op으로 재사용한다. 같은 correlation의 body가 다르거나 2개 이상이면 중복을 만들지 않고 fail-closed한다.
- Comment create timeout·오류 뒤에는 blind retry하지 않는다. Complete comment collection을 한 번 authoritative하게 reconcile해 exact result 하나가 확인될 때만 성공으로 판정한다.
- 게시 또는 재사용 후 exact comment ID, author stable account ID, body, created·updated timestamp, marker, digest, verified endpoint target, fresh PR metadata binding과 PR top-level collection cardinality를 readback한다. 신규 게시에서는 scoped PR top-level comment collection에 exact comment 하나만 추가되어야 한다. Provider의 aggregate comment-count field는 명시된 version-specific 계약으로 설명 가능해야 하며 설명할 수 없으면 fail-closed한다. `gitea-conversation-v1` cache opt-in은 [cached conversation admission](references/readback.md#cached-conversation-admission)에 따라 snapshot의 bounded stale lag만 허용하고, 신규 게시 후에는 실제 전체 conversation 개수와 exact-match를 요구한다. Cached baseline에 무조건 `+1`을 적용하지 않으며 reuse의 counter·metadata 불변 조건은 유지한다. 신규 게시에서 PR·underlying PR-Issue의 `updated_at`은 불변이거나 exact 신규 comment 생성 timestamp와 같을 때만 허용한다. 이는 다른 native metadata가 불변인 상태의 state-delta/exact-timestamp admission이며, GET readback만으로 독립적인 causal event attestation을 제공하지 않는다. Source Issue `<issue-number>`는 `updated_at`을 포함한 전체 native metadata와 comment collection이 완전히 불변이어야 하며, provider relation을 이유로 timestamp 예외를 허용하지 않는다. Existing exact comment 재사용에서는 PR top-level collection cardinality, PR·underlying issue와 Source Issue field가 모두 불변이어야 한다. 기존 review collection 또는 그 밖의 PR field 변경은 허용하지 않는다.
완료 조건: exact final head와 finding matrix에 결합된 canonical comment가 impl role author로 정확히 하나 존재하고 authoritative readback을 통과한다.
### 정본 provider readback 실행
- `references/readback.md`의 canonical `scripts/rework_comment.py snapshot`으로 게시 전 baseline을 수집하고 producer의 SHA-256을 독립 보존한다. Impl role의 private credential, stable account ID와 reviewed provider-contract fingerprint를 고정한다.
- 승인된 단일 게시 또는 existing-comment 재사용 뒤에는 `scripts/rework_comment.py verify-live`를 `terminal`로 실행한다. Source Issue·PR·underlying PR-Issue·scoped top-level comments·reviews·inline evidence를 fresh GET으로 다시 수집하고 계산된 불변성·cardinality 결과를 기존 verifier에 직접 연결한다.
- 정본 출력은 caller가 admission한 Issue runtime의 `rework-baseline.json`과 `rework-receipt.json`이다. Product checkout 밖에서 baseline은 보존하고 receipt는 성공 검증 시에만 update-in-place한다. 실패 때문에 baseline 재수집이나 POST 재시도를 하지 않는다.
- Provider version과 aggregate semantics는 명시된 trusted contract에서만 채택한다. Count-header 없는 empty-array와 capability-bound count fallback은 구분하며, 권한 오류·redirect·불완전 pagination은 fallback 사유가 아니다. Counts-only inline fallback을 complete inline-body inventory로 보고하지 않는다.
- 기존 offline `verify`는 호환 경로다. 수동 JSON의 success flag나 archived replay만으로 fresh provider readback 또는 외부 게시 완료를 주장하지 않는다.
## 절차
### Evidence manifest automation
모든 baseline, 통합·precommit 및 readback 검증은 `scripts/evidence_manifest.py`와 `templates/evidence-plan.json`을 사용해 exact argv, exit code, 제한된 output, 허용된 환경, source revision, 파일 hash와 표준 JSON-pointer claims를 기록한다. 명령은 argv 배열만 허용하고 명시된 최소 child-environment allowlist에서 실행하며 output 상한을 넘는 process는 중단한다. Secret·header·private-key·password 값이나 plan 금지 패턴이 발견되면 redaction하지 않고 fail-closed한다. `run`은 baseline부터 최종 precommit까지 사용하고, `validate`는 제품 tree의 evidence exclusion 없이 strict clean checkout에서 PR claim 생성 전에 실행한다. 새 기록은 external verification root의 v2이며 exact current HEAD와 source blob·hash를 다시 검증한다. 과거 evidence-only descendant는 명시적 archived v1 read-only 호환 경로에 한정한다. 수동으로 매칭되지 않는 PR 성공·검증 주장을 만들지 않는다. Comment publish 전 remote immutable artifact의 digest를 authoritative readback으로 비교한다.
### Conditional pytest evidence recipe
pytest를 이미 사용하는 프로젝트의 evidence command에만 적용한다. unittest 예시나 제품의 pytest 설정을 일괄 변경하지 않고, 승인된 interpreter·pytest 버전·plugin 및 기존 test target을 사용한다. 아래 JSON argv의 `{python}`과 `{tests}`를 exact interpreter와 승인 target으로 치환한다. 여러 target은 각각 argv 원소로 명시하고 shell 문자열로 합치지 않는다.
```json
["{python}", "-B", "-m", "pytest", "-o", "addopts=", "--color=no", "-rA", "{tests}"]
```
- 이 recipe는 command-local `addopts=`로 프로젝트의 quiet 누적 옵션을 해제하여 최종 summary를 보존하는 예시다. 기본적으로 `PYTEST_ADDOPTS`는 기존 child-environment allowlist에 없으며 전달하지 않는다. Quiet 자체가 항상 summary를 누락시키는 원인이라고 단정하지 않는다. 프로젝트 `addopts`의 필수 실행·plugin 옵션은 사전에 확인하여 필요한 옵션을 승인된 argv에 명시적으로 옮긴다. 필수 semantics를 보존할 수 없으면 recipe를 그대로 적용하지 않고 `evidence_gap`으로 남긴다. Warning 필터·skip 조건·선택 대상·제품 config 파일은 변경하지 않는다.
- 실제 승인 환경에서 먼저 최종 passed/skipped/failed 및 warning 요약의 보존 여부를 확인한다. pytest가 생략하는 zero category를 출력된 수치처럼 보고하거나 임의로 0으로 채우지 않는다. 이전 실행의 수치나 progress 문자 개수로 이번 실행 합계를 보충하지 않는다. Recipe만으로 모든 pytest 버전·plugin의 output 보존을 보장하지 않는다.
- 기존 evidence plan의 finite `timeout_seconds`, `max_output_bytes`, expected exit code와 process cleanup을 유지하고 exact argv·환경·source SHA·현재 실행 output을 기록한다. `timeout`·`output_limit`·불완전 summary이면 수치 claim은 `evidence_gap`이다. 명령이 완료되고 exit 0인 사실과 정확한 수치 보고의 증거는 구분한다. 승인 없이 재실행하거나 output 한도를 늘리지 않는다.
- 별도의 parser, runner, manifest schema 또는 pytest 설치 의무를 추가하지 않는다. Synthetic regression은 격리된 pytest interpreter에서 실제 pass/skip/warning과 failure 출력 및 기존 bounded reader의 보존을 검증한다. Pytest가 없는 test interpreter에서는 실행 회귀가 명시적으로 skip되므로 문서 계약 테스트만 통과한 결과를 실제 pytest 검증으로 보고하지 않는다. Synthetic test의 plugin autoload 차단은 fixture 격리용이며 제품 실행 정책이 아니다.
### 1. Target, identity와 review 고정
1. Exact target identifier를 검증하고 broker `serve`의 ready 응답과 public `request` API로 Issue lease를 확인한다.
2. Canonical authenticated Gitea adapter로 impl role identity, repository, Issue, PR, commits, changed files, statuses, workflow runs, 전체 reviews와 review comments를 읽는다.
3. 입력 승인 gate를 적용하고 Issue·PR·review snapshot을 저장한다.
4. Review marker에 head, contract digest, result digest 또는 correlation이 있으면 형식과 authoritative 값의 일치를 검증한다. Marker가 없는 code review는 review ID·author·commit binding과 finding body를 그대로 보존한다.
5. Finding matrix를 만들고 `blocked_spec` 또는 판정 불가능한 finding이 있으면 수정 전에 중단한다.
완료 조건: exact review와 reviewed head, current PR head, Issue contract digest와 finding matrix가 동일 snapshot에 결합돼 있다.
### 2. Cache와 기존 worktree 검증
1. `read_file`로 `operations/repository-cache.md`를 새로 읽는다.
2. Issue lease를 유지한 채 같은 broker handle로 `acquire_repository` request를 보내 cache path·ownership·permission·bare·remote를 검증한 뒤 origin에서 prune/fetch한다.
3. Gitea default branch와 local ref, PR base·head object 및 remote head branch SHA를 대조한다. Fork PR이면 target cache에 head object가 있다고 가정하지 않고 authoritative head repository와 branch를 별도로 검증한다.
4. Broker Issue lease와 repository lease를 모두 유지한 상태에서 기존 Issue worktree의 state identity, Git registration, common directory, detached·locked 상태, current HEAD, index, tracked·untracked state와 미완료 Git operation을 검증한다.
5. Worktree HEAD와 PR head SHA가 다르거나 dirty state가 snapshot으로 설명되지 않거나 다른 실행의 lock·Git operation이 있으면 reset·checkout·clean으로 복구하지 않고 중단한다.
6. Worktree admission 검증이 모두 끝난 뒤 `release_repository` request를 보내고 일반 편집·test·commit 동안 Issue lease만 유지한다.
7. 다른 checkout, 임시 clone 또는 경쟁 worktree를 만들지 않는다.
완료 조건: 기존 worktree가 exact owner, repository, Issue, impl role, PR branch와 reviewed head에 결합되고 보존할 모든 기존 상태가 기록돼 있다.
### 3. 재작업 계획과 baseline
1. 각 `valid_fix`와 `evidence_gap`을 승인 계약의 acceptance criterion에 역추적한다.
2. 변경 allowlist를 source, 문서, 설정, test와 evidence path로 만든다. 불변성 또는 evidence 재사용 검증에는 계약을 실제로 소유하는 exact path·제한된 glob만 별도 기록하며 package 전체처럼 넓은 경로를 근거 없이 사용하지 않는다. Review body의 path를 존재 검증 없이 신뢰하지 않는다.
3. Repository의 canonical 문서 README·TEMPLATE, build·test·lint 명령과 변경 대상 파일을 다시 읽는다.
4. 현재 reviewed head에서 관련 test와 review가 요구한 runtime check를 baseline으로 실행하고 command, exit code, output, environment와 HEAD를 기록한다.
5. Framework·runtime·protocol capability가 불명확하면 production edit 전에 최소 spike로 검증한다.
6. Review가 실제 browser 동작을 요구하면 framework test client나 protocol client로 대체하지 않는다. 사용할 수 있으면 Playwright MCP `mcp__playwright__browser_*`로 실제 Chromium 흐름과 browser storage·cookie·navigation을 검증하고, 사용할 수 없으면 해당 criterion을 unresolved로 유지한다.
완료 조건: 각 remediation item에 exact path, 실패 또는 부족한 baseline evidence와 완료를 증명할 검증 방법이 연결돼 있다.
### 4. Test-first 수정
1. `valid_fix`마다 결함을 재현하는 가장 작은 regression test를 먼저 추가하거나 기존 test가 의도한 이유로 실패함을 확인한다.
- Test file과 test function 이름은 추적 번호가 아니라 안정적인 기능·동작·불변조건을 표현한다. Issue, PR, review, comment 번호나 일시적인 finding ID를 이름에 넣지 않는다. 추적성은 finding matrix, commit, PR comment와 evidence metadata에 기록한다. 기존에 이미 존재하는 번호 기반 이름은 해당 재작업에서 별도 rename 범위를 승인받지 않았다면 유지한다.
- 동시성·event-loop finding은 wall-clock 의존을 최소화한 deterministic unit test와 실제 delay를 활성화한 짧은 runtime probe를 모두 실행해, 지연 semantics 유지와 무관한 coroutine·health 요청의 응답성을 각각 검증한다.
- Streaming request-body 상한 finding은 `Content-Length`를 신뢰하지 않고 실제 streamed bytes로 exact limit 허용, limit+1 거부, 누락·부정확한 `Content-Length`, 초과 뒤 `receive` 미호출을 검증한다. Upstream이 이미 전달한 단일 대형 chunk의 allocation은 애플리케이션이 막을 수 없다는 잔여 경계를 기록한다.
2. Source를 최소 변경해 test를 통과시키고 이미 satisfied인 criterion과 제외 범위를 보존한다.
3. `evidence_gap`은 production code를 불필요하게 바꾸지 않고 누락된 unit, integration, runtime, browser 또는 provenance evidence를 보강한다.
4. 보안 finding은 정상 경로뿐 아니라 fail-closed, no-call·no-write, 경계 시간, rotation 양방향, fresh session, logout 후 재접속과 credential 비노출을 해당 계약 범위에서 검증한다.
5. 문서 finding은 기존 canonical requirement ID와 revision history를 유지하고 요구사항, interface, project structure, runtime flow, deployment와 test를 일관되게 갱신한다. Review를 맞추기 위해 별도 요구사항 ID를 만들지 않는다.
6. 실제 credential, 운영 데이터 또는 민감 token을 fixture, evidence, log, screenshot, URL이나 repository file에 기록하지 않는다.
7. 각 item을 수정할 때 focused test를 실행하고 matrix의 `result`와 evidence를 갱신한다.
완료 조건: 모든 `valid_fix`가 재현 test와 수정 후 통과 evidence를 가지며, `evidence_gap`은 요구된 실제 검증 수준으로 채워졌거나 unresolved 이유가 명시돼 있다.
### 5. 통합 검증과 독립 검토
1. Repository가 선언한 formatter, lint, type check, build, targeted, integration과 relevant full suite를 실행한다.
2. Runtime·protocol·browser criterion은 가능한 가장 실제에 가까운 경로로 실행한다. 하위 수준 대체 evidence를 동등하다고 주장하지 않는다.
재작업 diff가 문서 파일에만 한정되고 기존 browser evidence가 결합된 source, test와 evidence path가 reviewed head부터 final head까지 byte-identical이며 ancestry를 검증한 경우에만 browser를 다시 실행하지 않고 기존 evidence를 재사용할 수 있다. 이때 비교한 exact path 집합과 양 head의 blob OID 또는 digest를 검증 기록과 결과 comment에 남긴다. 변경 경로가 겹치거나 동일성을 증명할 수 없으면 재사용하지 않는다.
3. Base-to-worktree와 reviewed-head-to-worktree diff를 모두 읽고 모든 changed file을 finding matrix와 acceptance matrix에 대응한다.
4. 범위 밖 refactor, debug artifact, generated drift, credential, 운영 데이터, test-only bypass와 예상하지 못한 삭제를 제거한다.
5. 독립 검토를 요청하기 전에 모든 actionable finding을 하나의 통합 adversarial suite로 실행한다. 각 finding은 실제 public response shape와 모든 직접 소비 경로를 포함해야 한다. 최소 검증 항목은 최초 실행과 idempotent replay의 응답 동등성, top-level/nested 필드 일치, 재귀적 민감정보 redaction, missing/initialized DB의 no-write, public positional signature, 동시성 및 실패 envelope다. 이 suite가 통과하지 않으면 독립 검토를 시작하지 않는다. 독립 검토는 새 결함을 처음 찾는 단계가 아니라 exact tree의 확인 단계다.
6. Security-sensitive 변경은 `delegate_task`로 bounded reviewer에게 exact worktree, reviewed head, contract subset, finding matrix와 file allowlist를 제공해 독립 검토한다. Gitea write, Git cleanup, commit과 push는 금지한다.
7. Delegate finding을 main agent가 직접 재현하고 유효한 문제만 수정한 뒤 영향을 받은 focused test와 relevant full suite를 다시 실행한다.
완료 조건: 모든 modified file이 approved finding 또는 필수 supporting change로 설명되고 unresolved finding과 검증 한계가 분리돼 있다.
### 6. Drift 확인과 commit
1. Issue, comments, PR, commits, changed files, branch SHA와 전체 reviews를 다시 읽는다.
2. Issue body digest·`updated_at`, PR base·head, 대상 review binding, review baseline 또는 default branch가 snapshot에서 material하게 drift하면 새 상태를 자동 채택하지 않고 중단한다.
3. Worktree의 staged·unstaged·untracked state와 미완료 Git operation을 확인한다.
4. Contract Preservation Inventory가 필요한 경우 final tree에 다시 대응해 baseline 항목 수와 실제 상태를 검증하고 `missing=0`, `ambiguous=0`이 아니면 stage하거나 commit하지 않는다.
5. Exact allowlist만 stage하고 staged diff를 다시 읽는다.
6. Commit 전에 subject와 body를 공통 [commit-message reference safety](../../../../operations/repository-cache.md#commit-message-reference-safety) 규칙에 따라 확인한다. Protected Issue metadata가 immutable이어야 하면 hash-number auto-reference와 parser-triggering closing/reopening 문구를 피하고 `Issue 18 / PR 19 / review 76` 또는 evidence로 추적한다. 기본은 새 non-amended commit이며, amend는 명시적 사용자 승인·미게시 증명·tree 동일성 증명과 새 SHA exact verification이 모두 있을 때만 허용된다. 일반 amend 권한으로 해석하지 않는다.
7. Commit 후 clean HEAD에서 최종 formatter, test와 필요한 runtime·browser 검증을 다시 실행한다. 이 뒤 repository file을 바꾸면 영향을 받은 검증을 다시 실행한다.
8. 최종 검증 기록은 exact HEAD 또는 검증한 source tree, command, exit code, pass·skip·fail 수치, 환경, relevant output과 reviewer가 접근 가능한 evidence location을 포함하며, PR body·결과 comment의 각 실행 주장과 수치는 해당 location에 일대일 대응시킨다. Hermes 내부 tool transcript, delegate 자기보고, 삭제 예정 임시 파일, reviewer가 접근할 수 없는 local path와 요약 수치만 있는 기록은 evidence로 인정하지 않고 `evidence_gap` 또는 `partial`로 유지한다. 새 evidence는 checkout 밖 정본 verification root에 보존하여 final HEAD를 기록하며 evidence-only descendant를 만들지 않는다. 과거 tracked evidence의 archived 분석은 fresh 실행·원격 head 검증과 구분한다.
완료 조건: final local HEAD, clean state, finding별 해결 evidence와 단일 검증 기록이 일치하며, 필요한 Contract Preservation Inventory는 `missing=0`, `ambiguous=0`이다.
### 7. 기존 PR branch 전달
1. Write 직전에 remote branch와 PR head가 snapshot의 reviewed head와 여전히 exact-match하는지 확인한다.
2. 첫 delivery push 전에 승인된 noninteractive credential capability를 preflight하고 공통 [non-file Git credential helper](../../../../operations/repository-cache.md#non-file-git-credential-helper)를 role-owned short-lived child process에서 사용한다. Credential은 Git의 private protocol pipe로만 공급하고 출력·파일·config에 저장하지 않으며 interactive/다른 helper fallback을 비활성화한다. 성공·실패와 무관하게 child process를 wait/reap하여 credential 환경의 종료를 확인한다. 그 후 canonical mirror remote를 변경하지 않고 explicit repository URL과 refspec으로 기존 Issue 전용 branch에 fast-forward non-force push한다. Named mirror remote를 사용해야 하면 `operations/repository-cache.md`에 따라 해당 push 호출에서만 `remote.<name>.mirror=false`를 적용한다.
3. Push 결과만 신뢰하지 않고 remote branch SHA와 PR head SHA가 final local HEAD와 exact-match하는지 readback한다.
4. PR body 갱신이 필요한 경우 write 직전에 PR 전체 필드, review·comment·commit collection cardinality와 authenticated impl identity를 다시 읽는다. Current body의 `Closes #<issue-number>`와 기존 구현 내용을 보존하고 재작업 review ID·reviewed head·finding별 조치·최종 검증과 unresolved limitation만 반영한 exact body payload를 명시해 한 번만 호출한다. 성공 응답만 신뢰하지 않고 fresh PR과 관련 collection을 읽어 PR author stable account ID, exact body payload, `updated_at`, base·head·commit/ref, state·draft·merge 상태, labels·assignees·reviewer와 collection cardinality를 baseline과 비교한다. Exact body와 provider가 해당 body update로 파생한 `updated_at` 외의 변경은 승인되지 않았으며 인과를 증명할 수 없으면 partial로 중단한다.
5. PR changed files, commits, state, merge 상태, statuses와 workflow runs를 다시 읽는다. Local test, commit status와 Actions run을 별도 사실로 기록한다.
6. Finding matrix와 최종 검증 기록을 bounded JSON input으로 만들고 `scripts/rework_comment.py render --input <input.json> --comment <comment.md> --expected <expected.json> --delegation-adoption <absolute-main-declaration.json>`를 실행해 canonical 재작업 결과 comment, marker-free digest와 correlation을 생성·검증한다. Comment body와 marker를 수동 조립하지 않는다.
7. Exact PR top-level comment endpoint target과 fresh PR metadata binding을 검증해 Source Issue comments, inline comments, review replies와 native reviews에서 분리하고 scoped collection을 complete pagination한 뒤 canonical deduplication 규칙을 적용한다. Existing exact comment가 없으면 exact PR conversation에 body를 한 번 게시하고, 있으면 write 없이 재사용한다.
8. Exact comment와 scoped complete collection을 fresh normalized JSON으로 readback한 뒤 `scripts/rework_comment.py verify --expected <expected.json> --pr-readback <pr.json> --comments-readback <comments.json> --identity-readback <identity.json> --action <create|reuse> --baseline-count <N> --output <handoff-receipt.json>`를 실행한다. Receipt의 comment ID, impl author stable account ID, body, timestamp, final head, digest, correlation과 cardinality가 검증돼야 하며, 신규 게시로 허용된 PR-underlying-issue projection 외의 PR field, Source Issue field와 기존 review collection에 변경이 없어야 한다.
9. Native review를 작성·dismiss·reply하거나 새 review를 요청하지 않는다. 기존 `REQUEST_CHANGES`는 reviewer가 새 head를 재검토하기 전까지 자동 해소된 것으로 주장하지 않는다.
10. Push, PR body 또는 comment write가 timeout·오류이면 blind retry하지 않고 branch, PR과 complete comment collection을 authoritative하게 reconcile한다. Exact write가 반영됐음을 증명할 수 없으면 partial 상태로 중단한다.
완료 조건: 같은 PR의 remote head가 final local HEAD와 일치하고 canonical 재작업 결과 comment가 정확히 하나 readback됐으며, `gitea-rework-handoff/v1` receipt가 `status=verified`, `readback_verified=true`로 생성되고 모든 authorized external write와 후속 review 필요성이 명확하다. Receipt가 없거나 검증에 실패하면 lock은 정상 해제하되 결과를 `partial|failed`로 보고하며 `completed`를 주장하지 않는다.
### 8. 종료와 다음 review handoff
1. 임시 검증 자원을 정리하기 전에 relevant output을 redaction하고 exact HEAD·command·exit code·환경과 결합해 reviewer-accessible evidence location에 저장한 뒤 그 내용을 readback한다. 그 후 임시 server, probe script·raw output, browser context, fixture workspace와 credential helper를 정리하고 부재를 확인한다.
2. 현재 실행이 시작한 tracked broker process와 그 자식 process를 cleanup 대상으로 관리한다. Shell PID 종료만으로 해제를 판정하지 않으며, 보유한 repository lease를 `release_repository`하고 Issue lease를 `release`한 뒤 broker를 wait/reap하고 `probe`에서 `free=true`를 확인한 경우에만 cleanup 완료로 판정한다.
3. Finding matrix, final diff, 검증 기록, remote head와 CI/status를 다시 대조한다.
4. 모든 item이 `resolved`여도 reviewer의 새 head 검토 전에는 구현 승인 완료가 아니라 `재작업 전달 및 comment handoff 완료, 재검토 대기`로 보고한다.
5. 후속 review가 새 head에 `REQUEST_CHANGES`를 남기면 그 review를 새 immutable snapshot으로 하는 별도 재작업 iteration을 시작한다. 이전 review snapshot이나 matrix를 새 head에 그대로 재사용하지 않는다.
완료 조건: resource와 lock 정리가 검증되고, 재작업 결과가 exact final head와 다음 review 입력으로 전달 가능하다.
## 위임 규칙
- Main impl-agent가 contract 해석, finding 분류, drift 판정, 최종 diff, Git·Gitea write와 delivery에 책임을 진다.
- 독립적인 source 조사, test 작성, disjoint 모듈 수정, browser evidence 수집, 실패 분석과 security review는 bounded delegate에 위임할 수 있다.
- Delegate에는 exact worktree path, reviewed head, contract subset, finding IDs, file allowlist와 검증 target을 제공한다.
- 같은 파일을 병렬 worker에게 주지 않고 Gitea write, lock·worktree 변경, commit, push, reset과 cleanup을 금지한다.
- Delegate summary를 evidence로 사용하지 않고 main agent가 diff, command output, browser 결과와 artifact를 직접 검증한다.
- 외부 side effect와 credential 사용은 main agent만 수행하고 즉시 authoritative readback한다.
## 금지 사항
- Review finding을 승인 계약보다 우선하거나 새 요구사항으로 승격하지 않는다.
- `blocked_spec`을 임의 설계 선택으로 해결하지 않는다.
- 이전 head의 stale review를 current head의 재작업 근거로 자동 적용하지 않는다.
- 기존 dirty worktree를 reset, clean, stash, checkout, switch, rebase 또는 삭제하지 않는다.
- Review를 해결한다는 이유로 already-satisfied 동작, security gate, validation 또는 실패 경로를 약화하지 않는다.
- Unit test, framework AppTest, HTTP client 또는 WebSocket client를 실제 browser evidence로 표현하지 않는다.
- PR body의 성공 주장, process exit code, delegate 보고 또는 push 응답만으로 해결을 주장하지 않는다.
- Canonical 재작업 결과 comment 이외의 PR comment, inline comment 또는 review reply를 작성하지 않는다.
- Existing `REQUEST_CHANGES` state를 직접 dismiss하거나 reviewer 대신 승인 완료로 판정하지 않는다.
- 새 PR, 새 branch, merge, Issue 변경, release 또는 deployment를 수행하지 않는다.
## 검증 및 보고
완료 보고에는 다음을 구분해 포함한다.
- authenticated impl identity와 exact repository, Issue, PR
- Issue contract `updated_at`, body digest와 review가 참조한 contract digest
- 대상 review ID·author·state·reviewed head·stale 상태와 current pre-rework head
- Finding별 classification, 계약 근거, 수정 path, test와 `resolved|unresolved` 결과
- Worktree path·identity, reviewed-head 시작 상태와 final local HEAD
- Baseline, focused, full-suite, runtime과 browser 검증의 exact command·exit code·결과
- 실제 browser가 요구된 경우 사용한 browser runner와 발급·복원·이동·삭제 등 관찰 결과. 기존 evidence를 재사용했다면 비교한 exact path 집합과 reviewed/final head의 blob OID 또는 digest
- Base 및 reviewed head 대비 전체 modified file과 범위 근거
- Local commit, remote branch와 PR final head SHA readback
- PR body write 여부, status와 Actions run의 success·failure·pending·없음 상태
- 재작업 결과 comment ID·URL·author·result·digest·correlation, exact final head binding과 verified handoff receipt
- 기존 review가 유지됨과 새 head의 후속 review 필요 여부
- 수행하지 않은 review·merge·Issue·release·deployment write
- Baseline failure, unresolved finding, specification blocker, unavailable evidence와 partial write
- 임시 resource 정리와 lock 해제 확인
모든 finding이 해결되고 검증된 final commit과 canonical 결과 comment가 기존 PR에 전달돼도 후속 reviewer가 새 head를 검토하기 전에는 `승인 완료`라고 보고하지 않는다. Final head와 exact comment readback, 전체 diff, 실제 검증 evidence와 resource·lock 정리가 일치할 때만 `재작업 전달 및 comment handoff 완료`로 판정한다.
## Canonical external verification evidence
검증 실행 기록은 공통 [Repository cache 운영 지침](../../../../operations/repository-cache.md)의 정본 경로 `<harness-root>/repositories/<owner>/<repo>/worktrees/issue-<number>/verification/<run-id>`에 보존한다. Product `checkout`과 artifact root를 분리하며, 회차별 evidence를 제품 `docs/verification/`에 새로 커밋하지 않는다. 기존 정본 문서·테스트·fixture·추적 파일은 자동 이동·삭제·ignore하지 않는다.
인증된 Issue/cache/worktree admission을 마친 caller가 broker Issue lease 아래 evidence `run` 전에 live lease의 exact harness root·Issue identity를 인증하고 root를 준비한다. Stage별 새 run-id를 사용한다. 아래 prepare는 `verification/`과 `<run-id>/`를 각각 별도 생성하고 각 directory를 실행 UID 소유 mode `0700`으로 만들며 즉시 `lstat`한다. Symlink, owner·mode 불일치와 collision은 거부하고 기존 malformed root를 자동 chmod하거나 repair하지 않는다. 이 경로에는 중간 parent가 umask를 따르는 `Path.mkdir(parents=True, mode=0o700)`를 사용하지 않는다. Helper는 lease/cache를 획득·복구하지 않는다. `<harness-root>`는 실제로 읽은 contract path로 고정하고 다음 변수는 그 root와 인증된 Issue/run identity에서 결정한다. 아래 명령은 harness root에서 호출한다.
```sh
python3 harness/lib/verification_root.py prepare \
--root "$HARNESS_ROOT" --owner "$OWNER" --repository "$REPOSITORY" \
--issue "$ISSUE" --run-id "$RUN_ID" --handle "$HANDLE"
python3 harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py run \
--contract-path "$HARNESS_ROOT/harness/operations/repository-cache.md" \
--repo-root "$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/checkout" \
--artifact-root "$VERIFICATION_RUN_ROOT" \
--plan evidence-plan.json --output evidence-manifest.json
python3 harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py validate \
--contract-path "$HARNESS_ROOT/harness/operations/repository-cache.md" \
--repo-root "$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/checkout" \
--artifact-root "$VERIFICATION_RUN_ROOT" \
--manifest evidence-manifest.json
```
`VERIFICATION_RUN_ROOT`는 `$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/verification/$RUN_ID`의 exact 절대 경로다. Plan의 `hash_paths`만 repository-relative이고 plan/output/manifest는 run-relative다. Source filename과 artifact filename이 같아도 같은 파일로 취급하지 않는다. `run`은 **strict clean checkout**, evidence **exclusion 없음**, full SHA·source/plan digest와 실제 command/exit code/bounded output binding을 요구하고 `evidence-manifest/v3`를 게시한다. Existing manifest를 덮어쓰지 않는다. `validate`도 같은 roots와 exact current HEAD를 요구한다. Dirty 개발 tree에서 validation 성공을 주장하지 않으며, final tracked 산출물을 commit한 뒤 그 exact commit을 검증하고 실패하면 push하지 않는다.
PR/결과 보고에는 stage/run-id, source SHA, manifest·plan digest, 실제 command/result/limitations와 reviewer 접근 수단을 함께 기록한다. Same-UID reviewer는 기록만 read-only로 읽고, distinct-UID/host이면 승인된 credential-free read-only export와 digest/readback receipt를 제공한다. 경로만 제시하거나 권한을 완화하는 것은 접근 검증이 아니다. 확인할 수 없으면 `evidence_gap`으로 보고한다. Product 테스트 증거와 harness 자체 검증을 구분하고, 인증정보 값은 `[REDACTED]`로만 기록한다.
Remote branch/PR head를 authoritative하게 읽어 source SHA·exact artifact와 결합하고 cleanup 전 실제 record readback을 확인한다. Helper의 local validate가 Gitea readback을 대신하지 않는다. Checkout 제거와 verification 폐기는 분리하며 완료 기록은 보존한다. 기존 v1은 `validate-legacy`의 read-only 호환 경로만 사용하고 새 publication proof로 사용하지 않는다.
### Tracked bounded background evidence execution
Full suite가 포함되어 foreground tool 시간창을 넘을 것으로 예상되는 evidence plan은 기존 실행 도구의 **tracked background** 작업으로 시작한다. 새 scheduler나 process manager를 만들지 않는다. 실행 직전에 exact run-id, source SHA, plan digest, output path와 command를 기록하고 시작 응답의 process/session handle을 결합해 보존한다. Handle이나 PID만으로 source/admission 또는 실행 성공을 판정하지 않는다. Broker Issue lease와 기존 admission 조건은 그대로 유지하며, lease를 유지·검증할 수 없으면 외부 handoff를 차단한다.
Background도 각 command의 `timeout_seconds`를 유지하고, main이 plan의 command deadline·정리 예산·실행 overhead를 고려해 유한한 outer deadline을 명시한다. 기존 도구의 tracked wait/poll·완료 통지를 사용하되 outer deadline 만료 시에는 기존 bounded process-group cleanup과 wait/reap 절차를 적용한다. Notification이나 manifest 파일의 존재만으로 완료를 선언하지 않는다. 실행 도구가 필요한 추적·deadline·정리 capability를 제공하지 않으면 시작하지 않고 blocker를 보고한다. Background가 세션 종료 이후의 지속성을 보장하지 않는다.
Foreground 응답 timeout이나 통지 누락은 실제 evidence process 종료 또는 제품 assertion 실패의 증거가 아니다. 자동 retry하지 않는다. 재실행 판단 전에 보존한 handle로 기존 실행의 상태·실제 exit code·process 종료와 cleanup을 확인하고, 같은 run-id의 manifest를 exact source SHA·plan digest·output path에 결합해 canonical `validate`로 확인한다. Validation은 process 종료·cleanup 확인을 대신하지 않는다. Tool 관측과 artifact가 상충하거나 실행 중·상태 불명·cleanup 미확인이면 `pending/partial`로 유지하고 중복 실행하지 않는다. 같은 실행의 상태 확인과 기록 정리만 계속하며 완료된 suite를 다시 수행하지 않는다.
종료·cleanup이 확인된 성공은 기존 evidence를 검증해 재사용한다. 실패는 실제 exit code와 bounded 부분 출력·termination 기록을 보존한다. Timeout·출력 초과·cleanup 실패를 TDD RED로 보고하지 않는다. 실패한 manifest가 `validate`를 통과하지 못하더라도 그 자체를 재실행 허가로 삼지 않는다. 기존 실행이 종료되고 cleanup이 확인된 실패에 대해서만 별도 승인 후 새 run-id로 실행한다. 기존 artifact를 덮어쓰지 않는다. 이 절차는 workflow 문서 계약이며 evidence runner/schema, lease/TTL, publication journal 및 자동 retry 정책을 변경하지 않는다.
Command deadline 계약: 새 plan의 각 command는 `timeout_seconds`를 명시한다. Boolean·문자열·누락·비유한 값·0 이하·3600 초 초과를 실행 전에 거절하며 암묵적인 무제한 실행을 허용하지 않는다. Monotonic wall-clock deadline은 무출력 대기, stdout EOF 이후 실행, leader 종료 뒤 descendant가 유지한 pipe까지 적용한다. 각 command는 별도 session/process group에서 실행하고 timeout·출력 초과·예외 시 해당 process group을 TERM → 제한된 grace → KILL로 정리하고 direct child를 bounded wait로 회수한다. Leader가 종료됐어도 같은 group의 살아 있는 descendant를 남기지 않는다. 이것은 sandbox가 아니며 별도 session으로 이탈한 descendant의 containment나 grandchild 직접 회수를 보장하지 않는다.
`evidence-manifest/v3`는 실제 timeout, `execution_status`, `termination`의 `term_sent`·`kill_sent`·`direct_child_reaped`·`group_cleanup_completed` boolean 기록과 안전한 bounded 부분 출력을 결합한다. 새 claim pointer는 기존 6개에 timeout·실행 상태·종료 기록을 추가한 9개다. Linux `/proc`로 같은 group의 non-zombie 프로세스를 확인하며 확인·정리에 실패하면 `cleanup_failed`로 차단한다. TERM grace는 0.25 초, KILL 후 group 종료·direct-child 회수 예산은 0.5 초이며 오류 fallback wait도 0.1 초로 제한한다. 새 command의 `max_output_bytes`는 0–1048576 범위의 non-boolean 정수만 허용한다. `execution_status`는 `completed|timeout|output_limit|cleanup_failed|descendant_cleanup`이다. 정상 종료 leader 뒤의 살아 있는 descendant를 신호로 정리한 경우도 `descendant_cleanup` 실패이며, `completed`에는 TERM/KILL 신호가 있을 수 없다. Timeout·출력 한도 초과·정리 실패는 expected exit code와 같아도 성공이 아니며 **TDD RED** 또는 제품 assertion 실패로 분류하지 않는다. 해당 실행은 실패로 보존하고 뒤의 command를 실행하지 않으며, 실행된 prefix의 claim만 기록한다. Partial output에도 기존 secret 탐지를 적용하고 인증정보 유출은 fail-closed한다. 자동 재실행은 하지 않으며 새로 승인된 실행에는 새 run-id를 사용한다.
기존 `evidence-manifest/v2`는 명시적 `validate-archived-v2 --contract-path <canonical-contract> --repo-root <checkout> --artifact-root <canonical-run> --manifest <run-relative-manifest>`로 read-only 호환 검증한다. Canonical identity, exact current HEAD, source/plan/output binding을 유지하지만 과거 기록에 없던 timeout이나 종료 보장을 추론하지 않는다. 새 `run`/`validate`에는 v2 fallback이 없다. v1은 기존 `validate-legacy`만 사용한다. 두 archive 경로의 성공은 새 실행·TDD RED·fresh publication proof가 아니며 기존 artifact를 수정하거나 새 schema로 자동 변환하지 않는다.
Required reference: [workflow](references/workflow.md). 행동 전에 `read_file`로 이 참조 전체를 읽어야 한다. 이 요약은 승인·lease·검증·delivery gate를 대체하지 않는다.
@@ -230,26 +230,13 @@ different-body cases remain fail-closed under ordinary create/reuse; this
correction exception is not permission to create another comment or broaden
reuse.
Only the main impl workflow, with separate explicit correction approval and the
active Issue lease, may execute the single PATCH. Immediately before that PATCH,
perform fresh unchanged checks against the correction snapshot: exact target,
comment ID, unique correlation/no duplicates, author identity, current body,
Source Issue, PR, underlying PR-Issue, reviews, inline evidence, complete
collections and provider contract must still match, with the expected API status
and version token derived from the matching source response. If any material
state drifted, the API lacks a trustworthy status/version precondition, or
one-time conditional PATCH semantics cannot be established, stop as partial;
do not substitute POST, retry, delete, or restore source state.
Default correction is blocked in this revision because there is no supported client-snapshot CAS execution/verification pair; a lease or capability flag is not provider CAS. Fail closed unless the user separately and explicitly approves the supported ordinary single PATCH exception with its residual read/write race disclosed. Future CAS support requires both execution and matching verifier proof before admission. For that approved path, the main impl workflow under the active Issue lease performs fresh preflight immediately before the one PATCH: exact target, comment ID, unique correlation/no duplicates, author identity, current body, Source Issue, PR, underlying PR-Issue, reviews, inline evidence, complete collections and provider contract must match. No POST, retry, delete, or source-state restoration is allowed. Report `atomic_client_precondition=false` and the residual race for ordinary PATCH.
After PATCH, read back the exact same comment ID and verify actor stable account
ID, immutable `created_at`, URL, full scoped collection count, exact corrected
body, recomputed result digest and unchanged marker identity/correlation. Prove
all old rows, reviews, inline evidence, Source Issue fields and entire metadata
(including `updated_at`), timeline, and cached counter are unchanged. Only this
comment's body/result hash/marker and a provider-verified comment `updated_at` or
revision may change, and only with the version/precondition proof. A PR or
underlying PR-Issue `updated_at` may change only when the exact timestamp is
explained by the provider's verified correction semantics. Any unexplained delta,
(including `updated_at`), timeline, and cached counter are unchanged by default. Strict no-timeline-change is the default. A separately approved observed reference-neutralization profile may admit only a pre-existing `comment_ref` pointing to this exact comment/PR transitioning `none` to `neutered`; ID, actor, `created_at`, and target remain immutable, and only `updated_at` plus the exact embedded corrected-comment body/updated_at projection may change. This is observed behavior, not proof of every deployed build; unchanged events remain structurally equal and any other timeline delta fails. Only this comment's body/result hash/marker and provider-verified comment `updated_at` or revision may change. A PR or underlying PR-Issue `updated_at` may change only when exact timestamp is explained by verified correction semantics. Any unexplained delta,
missing field, incomplete pagination or unsupported semantic is partial and
stops further action. Never delete comments, duplicate-POST, restore or rewrite
the Source Issue, perform a native review, or clean up source timeline history.
@@ -0,0 +1,388 @@
# Gitea Issue Rework
`gitea-issue-implementation`으로 전달한 기존 Gitea PR에 대해 `gitea-spec-agent` 또는 승인된 `gitea-review-agent`가 남긴 검토 결과를 exact reviewed head와 승인된 Issue 계약에 결합해 재작업한다. 유효한 지적만 source·문서·test·evidence에 반영하고 같은 Issue 전용 branch에 non-force push한 뒤, exact final head에 결합된 bounded PR comment로 재검토 handoff를 게시한다. 새 요구사항을 만들거나 명세를 구현에 맞춰 완화하지 않으며 merge, Issue 변경과 review 작성은 수행하지 않는다.
Gitea 인증과 authoritative readback은 [Gitea authenticated access 운영 지침](../../../../../operations/gitea-access.md), cache·lock·worktree 규칙은 [Repository cache 운영 지침](../../../../../operations/repository-cache.md), 문서 형식은 `docs/` 아래 각 문서 유형의 README와 TEMPLATE만 정본으로 사용한다.
## Test inventory source-binding gate
Baseline/current inventory를 생성·전달·채택할 때 `check-tests 이전`에 `harness/lib/test_inventory_binding.py`를 실행한다. Inventory의 자기 선언을 신뢰하지 않고 독립적으로 고정한 revision과 stage를 expected 입력으로 제공한다. Exact Git blob의 파일 존재·SHA-256 및 collection scope 결속을 [공통 source-binding 정본](../../../../../operations/repository-cache.md#test-inventory-git-source-binding-preflight)에 따라 확인하고, 통과한 ID/source가 이후 구조 guard 입력과 같은지도 대조한다. 불일치 증빙을 채택하거나 현재 collection을 baseline으로 재명명하지 않는다. PASS가 named test의 존재·collection 실행·의미 보존을 인증하지는 않으며 main의 실제 실행 provenance 검토와 기존 revision evidence 영향 평가를 유지한다. 이 gate는 제품 실행·별도 checkout 권한이나 자동 제품 변경 요청을 추가하지 않는다.
## Delegation evidence acceptance gate
위임 결과 채택 이전에 `harness/lib/delegation_evidence.py`를 [공통 증빙 채택 정본](../../../../../operations/repository-cache.md#delegation-evidence-acceptance-preflight)에 따라 실행한다. main이 독립적으로 고정한 context에 source/bundle digest, 허용 작업, 검증 항목과 독립 실행 catalog를 넣으며 worker 자기 선언으로 context를 만들지 않는다. Source line/named test anchor와 실제 보존된 회귀 테스트·RED/GREEN 기록을 대조하고, bundle 추출 범위 밖 부재 주장은 authoritative Git 조회 없이 채택하지 않는다. Import·환경 오류를 RED로 취급하거나 확인되지 않은 위임 주장을 최종 판정·delivery·publication 근거로 옮기지 않는다. Static-only worker가 기존 독립 실행 근거를 인용하는 것과 직접 실행하는 것을 구분한다. BLOCK/입력 거부이면 해당 증빙 채택을 중단하고 main이 실제 기록과 의미를 재검증한다. Gate는 sandbox, 실행 인증 또는 assertion 의미 판정기가 아니며 기존 compare-source/check-tests와 role의 제품 실행 제한을 대체하지 않는다.
실제 소비 함수에서도 [runtime consumption 정본](../../../../../operations/repository-cache.md#delegation-adoption-runtime-consumption)의 `require_adoption`을 통과한다. main은 위임 근거 비사용을 `{"mode":"none"}`, 사용을 `mode=delegated` exact 선언으로 명시하며 누락이나 이전 PASS receipt를 허용하지 않는다. 판정/render, delivery artifact, native publication마다 새 검증을 수행하고 소비 source/stage에 결속한다. 최초 구현 delivery는 `gitea-issue-implementation/scripts/implementation_delivery.py`의 `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`에서 main-owned push/PR adapter를 호출한다. Adapter는 push/read_remote_head/create_or_reuse_pr/read_pr를 제공하고 기존 인증·repository/Issue scope·non-force explicit refspec·remote readback 계약을 지킨다. 새 진입점을 쓰지 않는 직접 push/PR 생성은 이 workflow를 우회하는 것으로 금지한다. Gate 밖 직접 Git/API 실행을 sandbox로 차단한다고 주장하지 않는다. 기존 semantic/source/identity/lease 검증과 외부 readback은 그대로 유지한다.
## Managed-lock broker gate
Bootstrap 이후 이 Issue 재작업도 canonical [Repository cache 운영 지침](../../../../../operations/repository-cache.md)의 공통 managed-lock broker 하나만 사용한다. `serve`에 canonical Issue identity, role, workflow run-id와 TTL을 전달해 broker-generated runtime을 만들고 ready JSON의 `handle`/`socket` 경로만 사용한다. 경로는 `<issue>/runtime/<workflow-run-id>/broker.handle`과 `broker.sock`이며 verification run-id와 독립적이다. Runtime 생성은 approval이나 worktree admission을 대신하지 않는다. Cache bootstrap 이후 broker가 Issue lock 아래 runtime을 준비하고, ready 이후 각 workflow의 진입 검증·승인 조건을 확인한다. 같은 handle로만 public `request` API의 `acquire_repository`/`release_repository`를 요청하고 TTL 전에 `renew`한다. Client는 raw token을 inspect/log하지 않으며 broker request API가 token을 캡슐화한다. 정상·오류·cancel cleanup은 보유한 repository lease release, Issue `release`, broker wait/reap, `probe`의 `free=true` 확인까지 동일하며 그 전에는 completed/log/report를 주장하지 않는다. Raw `flock ... sleep`, lock-file unlink와 client-side lock holder는 금지한다.
## 사용 시점
다음처럼 exact repository, source Issue, 기존 구현 PR과 재작업할 review를 식별할 수 있는 요청에 사용한다.
```text
<owner>/<repository> Issue #<issue-number> 구현 PR #<pull-number>의 검토 지적을 재작업하라
```
이 요청은 아래 작업만 승인한다.
- 기존 canonical Issue worktree 검증과 재사용
- 승인된 계약 안에서 검토 지적을 해결하는 문서, source, test, 설정과 evidence 수정
- synthetic fixture와 test-only credential을 사용한 build, test, lint, 실제 runtime·browser 검증
- 기존 Issue 전용 branch 위의 non-amended commit 생성과 non-force push
- 현재 PR 본문의 기존 의미와 `Closes #<issue-number>`를 보존하면서 재작업 요약과 최종 검증 결과를 갱신하는 작업
- 이 스킬의 canonical 형식과 deduplication 계약을 만족하는 재작업 결과 PR comment 하나의 생성 또는 기존 exact comment 재사용
다음 작업은 별도 요청이 없으면 승인되지 않는다.
- 새 worktree나 대체 PR 생성, 다른 branch 또는 default branch로 push
- force-push, rebase, amend, reset, clean, stash 또는 history rewrite
- PR merge·close·reopen, native review·review reply·임의 comment 작성, reviewer 지정 또는 approval
- Source Issue `#<issue-number>`의 body·comment collection·title·state·label·assignee·milestone 변경. Canonical PR comment는 exact Pull Request `#<pull-number>`의 top-level conversation에만 쓰며, provider가 이를 PR 자체의 underlying issue record `#<pull-number>` comment로 저장하는 projection은 허용한다. Source Issue record와 PR underlying issue record를 identifier·object kind·pull relation으로 구분할 수 없으면 게시하지 않는다.
- 승인된 Discovery의 범위·결정·acceptance criterion 변경
- release, package 또는 deployment 게시
최초 구현, 독립 review, Discovery 변경 또는 이미 반영된 지적의 재검토에는 사용하지 않는다.
## 입력 승인 gate
초기 review decision gate는 [references/input-gate.md](input-gate.md)의 public `input-gate`를 **no_op 또는 source edit 전에 반드시 먼저** 호출한다. 이 gate는 선택된 source review나 renderer `expected.json`을 입력으로 요구하지 않는다. Receipt의 `rework` ID를 읽은 뒤 finding은 수동으로 검증하며 automatic `COMMENT` classification은 지원하지 않는다. Gate의 `no_op`은 구현 재작업을 건너뛴다는 뜻일 뿐 native review publisher의 idempotent review reuse/no-op과 다르다. Native publication, journals, lease/worktree admission, full Discovery approval semantics와 pre-write freshness는 이 receipt로 증명되지 않으며 독립적으로 수행한다.
Repository file을 수정하기 전에 다음을 모두 검증한다.
- `owner`, `repository`, Issue 번호와 PR 번호가 canonical identifier다.
- Impl role의 authenticated stable account identity와 credential provenance를 확인했다.
- PR은 open이고 source Issue와 exact `Closes #<issue-number>` 하나로 결합되며 PR author의 stable account ID가 현재 authenticated impl role의 stable account ID와 exact-match하는 기존 구현 PR이다.
- PR base는 target repository의 default branch이고 head repository·branch·40자리 full SHA를 식별할 수 있다.
- Current Issue body는 유효한 `gitea-agent:discovery-draft/v2` 계약이고 미해결 질문이 없으며 body SHA-256을 계산할 수 있다.
- 재작업 대상 native review의 ID, author stable account ID, state, body, reviewed commit SHA, stale·dismissed 여부와 inline comments를 complete pagination으로 읽었다.
- Review author가 허용된 spec 또는 code-review role이고 impl role과 다른 identity다.
- `REQUEST_CHANGES` 또는 명시적인 actionable finding이 있으며 각 finding의 계약 근거와 대상 경로를 판독할 수 있다.
- Review가 참조한 contract digest가 있으면 current Issue body digest와 exact-match한다.
- Reviewed head가 current PR head와 exact-match한다. 다르면 current head에 결합된 더 최신 authoritative review를 사용하고, 없으면 이전 지적을 새 head에 임의 적용하지 않고 중단한다.
- Changed-file, review, review-comment와 commit pagination이 완전하고 diff가 truncated·ambiguous하지 않다.
- Canonical Issue worktree와 PR head branch를 현재 실행이 안전하게 재사용할 수 있다.
Canonical input-gate의 `decision.rework_review_ids`는 현재 head·계약에 결합된 role별 최신 `REQUEST_CHANGES`만 포함한다. Spec 또는 code-review role 중 하나 이상에 유효한 최신 수정 요청이 있고 다른 차단 사유가 없으면 `rework`다. 다른 role의 유효 review 부재는 `missing_current_review`로 기록하되 이 수정 요청의 재작업 착수를 차단하지 않는다. 수정 요청 없이 review가 부족하면 `blocked`이며 no-op으로 종료하지 않는다. `rework`에서도 finding의 계약 근거·scope·독립 승인과 lease/worktree 조건을 별도로 확인한 뒤에만 수정한다. `COMMENT`는 자동 actionable 판정을 하지 않고 `blocked`로 남겨 main의 수동 분류 또는 별도 승인 절차로 넘긴다. 특히 `blocked_spec`, 계약 모순, 선택되지 않은 설계 선택 또는 source reality와 계약 충돌은 구현 재작업으로 해결하지 않으며 Discovery 재검토가 필요하다고 보고한다. `blocked` receipt로 source를 수정하거나 no-op을 주장하지 않는다. Current head·계약의 spec와 code-review role별 최신 유효 review가 모두 `APPROVED`인 fresh `no_op` receipt에서만 write 없는 종료를 한다. 다른 role의 승인·old-head/stale/dismissed review·본문의 해결 주장은 이 조건을 대체하지 않는다.
## Snapshot과 finding matrix
첫 write 전에 canonical collection의 test inventory와 영향을 받는 API·CLI·필드·상태·cleanup 의미를 snapshot/finding matrix에 연결하고 [공통 workflow guard 계약](../../../../../operations/repository-cache.md#shared-workflow-guard-gates)을 적용한다. `python3 <harness-root>/harness/lib/workflow_guard.py check-tests --input JSON`과 `check-contracts --input JSON`을 결과 채택 전에 실행한다. RED/GREEN records must be named: 각 유효한 finding은 current test ID와 실제 실행 기록에 결합하며 환경/import 오류나 반증된 지적에 가짜 RED를 만들지 않는다. 읽기 전용 검토에는 위임 전 source snapshot과 채택 전 비교를 요구하고, 승인된 쓰기 worker의 편집에는 diff·의미 검토를 적용한다. 기록은 제품 checkout 밖 정본 verification run에 보존하며 helper schema·증거를 검증한다. gate failure는 운영 근거 무효이지 자동 제품 결함이 아니다.
첫 write 전에 다음 immutable snapshot을 보존한다.
- Issue title, body, state, `updated_at`, labels, assignees, milestone, 전체 comments와 body SHA-256
- PR title, body, state, `updated_at`, base·head repository와 branch, base·head SHA, author, changed files, commits, status, workflow run, PR top-level comment collection의 exact cardinality와 provider가 노출하는 comment-count field baseline
- 전체 native review baseline과 대상 review의 ID, author, state, body digest, reviewed head, submitted time, stale·dismissed 상태와 inline comments
- Worktree state identity, base SHA, current HEAD, detached·locked 상태, staged·unstaged·untracked file과 미완료 Git operation
기존 canonical requirement revision을 갱신하거나 기존 계약 보존이 승인 조건이면 pinned source revision 또는 직전 승인 revision의 모든 requirement·acceptance criterion·상태 불변조건을 의미 단위로 추출해 다음 Contract Preservation Inventory를 같은 snapshot에 고정한다. ID 변경만으로 보존을 판정하지 않는다.
```text
baseline_revision: <full SHA>
path: <repository-relative canonical requirement path>
baseline_id: <기존 requirement 또는 AC ID>
semantic_key: <ID와 독립적인 안정된 의미 식별자>
meaning: <정상·실패·경계·불변조건의 bounded 설명>
reviewed_ids: [<reviewed head의 대응 ID>...]
reviewed_status: preserved | intentionally_changed | missing | ambiguous
planned_ids: [<재작업 후 대응 ID>...]
planned_status: preserved | intentionally_changed | missing | ambiguous
authorization: Issue contract citation authorizing intentional change, or none
evidence_paths: [<source/test/document path>...]
verification: <test/static/runtime/browser evidence>
```
Baseline 항목은 중복 없이 정확히 한 번 나타나야 한다. `intentionally_changed`에는 승인된 Issue 근거가 필요하다. `reviewed_status`의 `missing`은 finding matrix에 포함하고 `ambiguous`는 계약 판단 불가로 중단한다. Repository 첫 write 전 계획 기준 `planned_status`가 `missing=0`, `ambiguous=0`이어야 한다. Contract Preservation Inventory를 delegate할 때는 immutable pre-write snapshot에 대해 완료·회수한 뒤에만 mutating worker를 시작하며, 편집과 병렬 실행해 중간 상태를 판정하지 않는다.
모든 finding을 다음 matrix의 정확히 한 항목으로 분류한다.
```text
finding: <review-id>/<stable finding id>
reviewed_head: <full SHA>
contract: <Issue/requirement/acceptance criterion 근거>
classification: valid_fix | evidence_gap | reviewer_misunderstanding | blocked_spec
implementation_paths: [<repository-relative path>...]
test_paths: [<repository-relative path>...]
verification: <실행할 command/runtime/browser evidence>
result: pending | resolved | unresolved
reason: <bounded 판정 근거>
```
- `valid_fix`: 승인 계약을 위반한 source·문서·설정·test 결함이다.
- `evidence_gap`: 구현 적합성은 확인 가능하지만 exact-head-bound 실행·runtime·browser·CI evidence가 부족하다.
- `reviewer_misunderstanding`: 지적이 승인 계약 밖의 새 요구사항이거나 authoritative tree와 맞지 않는다. 이를 만족시키려고 범위를 확장하지 않고 근거와 함께 unresolved로 보고한다.
- `blocked_spec`: 명세 모순이나 새 결정이 필요하다. 모든 repository write를 중단한다.
완료 조건: 모든 review finding과 unsatisfied·indeterminate criterion이 matrix에 정확히 한 번 나타나고, `valid_fix`와 `evidence_gap`마다 변경 또는 검증 경로가 있으며, 이미 satisfied인 계약을 약화하는 항목이 없다. Contract Preservation Inventory가 필요한 경우 baseline 전체가 대응되고 `missing=0`, `ambiguous=0`이다.
## Canonical 재작업 결과 comment
재작업 branch를 전달한 뒤 reviewer가 새 head를 자동으로 재검토할 수 있도록 exact PR의 top-level conversation에 comment 하나를 게시한다. Adapter target은 object kind가 pull request이고 index가 exact `<pull-number>`인 record여야 한다. Provider가 PR comment를 underlying issue comment로 저장하면 exact comment endpoint target의 issue index가 `<pull-number>`이고 fresh PR metadata가 그 record를 같은 pull request로 식별함을 증명해야 한다. Comment object가 target kind를 반복하지 않는 API에서는 검증된 endpoint target과 PR metadata binding을 사용한다. Source Issue `<issue-number>`, inline comment, review reply 또는 native review로 대체하지 않는다. Marker를 제외한 body를 먼저 canonical UTF-8/LF bytes로 직렬화하고 그 SHA-256을 `result-sha256`으로 계산한다.
Canonical body와 marker는 `scripts/rework_comment.py render`로만 생성하고, 게시 또는 exact comment 재사용 뒤에는 authoritative normalized readback을 `scripts/rework_comment.py verify`로 검증한다. `status=verified`와 `readback_verified=true`인 `gitea-rework-handoff/v1` receipt가 없으면 완료 조건을 충족하지 않으며 PR body update나 write 성공 response는 receipt를 대체하지 않는다.
```markdown
## 구현 재작업 결과
- Source Issue: <issue-number>
- Pull Request: <pull-number>
- Source Review: <review-id>
- Reviewed head: `<reviewed-head-sha>`
- Rework head: `<final-head-sha>`
- Contract SHA-256: `<contract-sha256>`
- Result: `rework_delivered|partial`
### 지적 처리
- `<finding-id>` — `resolved|unresolved`
- 분류: `valid_fix|evidence_gap|reviewer_misunderstanding`
- 변경: <repository-relative paths 또는 없음>
- 검증: <bounded command, runtime, browser 또는 정적 evidence>
- 근거: <bounded reason>
### 최종 검증
- Local checks: <exact command와 exit code 및 pass·skip·fail 요약>
- Runtime/browser: <실행 결과 또는 해당 없음·미검증>
- Commit status: <success|failure|pending|없음|조회 불가>
- Actions runs: <success|failure|pending|없음|조회 불가>
### 다음 단계
Reviewer가 rework head를 새 snapshot으로 다시 검토해야 하며 이 comment는 기존 `REQUEST_CHANGES`를 해제하거나 승인을 의미하지 않는다.
<!-- gitea-agent:implementation-rework/v1 owner=<owner> repository=<repository> issue=<issue-number> pr=<pull-number> review=<review-id> reviewed-head=<reviewed-head-sha> head=<final-head-sha> contract-sha256=<contract-sha256> result-sha256=<result-sha256> correlation=<correlation> -->
```
- `rework_delivered`는 모든 `valid_fix`와 `evidence_gap`이 `resolved`이고 final branch delivery와 검증이 완료된 경우에만 사용한다. 그 외 전달 가능한 결과는 `partial`이며 unresolved 이유를 숨기지 않는다.
- Finding은 matrix 순서로 정확히 한 번 렌더링한다. 이미 satisfied였고 재작업하지 않은 criterion을 반복해 comment를 부풀리지 않는다.
- Path, SHA, enum, command와 수치는 검증된 값을 원문 그대로 사용한다. Human-readable text에는 credential, token, 실제 secret, mention, 외부 지시, HTML 또는 실행 가능한 payload를 포함하지 않는다.
- 모든 human-readable comment line에서 local 또는 cross-repository hash-number 자동참조 문법을 쓰지 않는다. Escape한 표기와 Markdown/code 안의 표기도 금지한다. Source Issue, Pull Request, Source Review 예시는 위처럼 plain decimal number로 쓴다. Hidden canonical marker의 identifier와 correlation 입력은 이 표시 규칙 때문에 바꾸지 않는다. Marker를 제외한 exact UTF-8/LF body에 대해 `result-sha256`을 다시 계산한다. 기존 PR body의 의도된 `Closes #<issue-number>`는 그대로 보존하며 변경하지 않는다.
- Marker-free body는 LF로 정확히 하나 끝나야 한다. 게시 body는 marker-free bytes, marker line과 최종 LF 하나를 순서대로 사용하며 trailing whitespace를 금지한다.
- `correlation`은 `owner`, `repository`, base-10 Issue 번호, base-10 PR 번호, base-10 review ID, lowercase reviewed head, lowercase final head와 lowercase contract digest를 이 순서로 NUL 하나씩 구분한 UTF-8 bytes의 SHA-256이다. `result-sha256`은 correlation 입력에 넣지 않는다.
- 게시 전 exact PR top-level comment endpoint target과 fresh PR metadata binding을 검증하고 그 scoped collection만 complete pagination한 뒤 같은 correlation을 찾는다. Source Issue comment endpoint, inline-comment·review-reply endpoint와 native review collection은 검색 집합에서 제외하며 endpoint scope와 PR relation을 구분할 수 없으면 fail-closed한다. 0개면 exact body를 한 번 생성하고, 1개이며 author stable account ID·body·marker·digest가 모두 같으면 write 없는 no-op으로 재사용한다. 같은 correlation의 body가 다르거나 2개 이상이면 중복을 만들지 않고 fail-closed한다.
- Comment create timeout·오류 뒤에는 blind retry하지 않는다. Complete comment collection을 한 번 authoritative하게 reconcile해 exact result 하나가 확인될 때만 성공으로 판정한다.
- 게시 또는 재사용 후 exact comment ID, author stable account ID, body, created·updated timestamp, marker, digest, verified endpoint target, fresh PR metadata binding과 PR top-level collection cardinality를 readback한다. 신규 게시에서는 scoped PR top-level comment collection에 exact comment 하나만 추가되어야 한다. Provider의 aggregate comment-count field는 명시된 version-specific 계약으로 설명 가능해야 하며 설명할 수 없으면 fail-closed한다. `gitea-conversation-v1` cache opt-in은 [cached conversation admission](readback.md#cached-conversation-admission)에 따라 snapshot의 bounded stale lag만 허용하고, 신규 게시 후에는 실제 전체 conversation 개수와 exact-match를 요구한다. Cached baseline에 무조건 `+1`을 적용하지 않으며 reuse의 counter·metadata 불변 조건은 유지한다. 신규 게시에서 PR·underlying PR-Issue의 `updated_at`은 불변이거나 exact 신규 comment 생성 timestamp와 같을 때만 허용한다. 이는 다른 native metadata가 불변인 상태의 state-delta/exact-timestamp admission이며, GET readback만으로 독립적인 causal event attestation을 제공하지 않는다. Source Issue `<issue-number>`는 `updated_at`을 포함한 전체 native metadata와 comment collection이 완전히 불변이어야 하며, provider relation을 이유로 timestamp 예외를 허용하지 않는다. Existing exact comment 재사용에서는 PR top-level collection cardinality, PR·underlying issue와 Source Issue field가 모두 불변이어야 한다. 기존 review collection 또는 그 밖의 PR field 변경은 허용하지 않는다.
완료 조건: exact final head와 finding matrix에 결합된 canonical comment가 impl role author로 정확히 하나 존재하고 authoritative readback을 통과한다.
### 정본 provider readback 실행
- `references/readback.md`의 canonical `scripts/rework_comment.py snapshot`으로 게시 전 baseline을 수집하고 producer의 SHA-256을 독립 보존한다. Impl role의 private credential, stable account ID와 reviewed provider-contract fingerprint를 고정한다.
- 승인된 단일 게시 또는 existing-comment 재사용 뒤에는 `scripts/rework_comment.py verify-live`를 `terminal`로 실행한다. Source Issue·PR·underlying PR-Issue·scoped top-level comments·reviews·inline evidence를 fresh GET으로 다시 수집하고 계산된 불변성·cardinality 결과를 기존 verifier에 직접 연결한다.
- 정본 출력은 caller가 admission한 Issue runtime의 `rework-baseline.json`과 `rework-receipt.json`이다. Product checkout 밖에서 baseline은 보존하고 receipt는 성공 검증 시에만 update-in-place한다. 실패 때문에 baseline 재수집이나 POST 재시도를 하지 않는다.
- Provider version과 aggregate semantics는 명시된 trusted contract에서만 채택한다. Count-header 없는 empty-array와 capability-bound count fallback은 구분하며, 권한 오류·redirect·불완전 pagination은 fallback 사유가 아니다. Counts-only inline fallback을 complete inline-body inventory로 보고하지 않는다.
- 기존 offline `verify`는 호환 경로다. 수동 JSON의 success flag나 archived replay만으로 fresh provider readback 또는 외부 게시 완료를 주장하지 않는다.
## 절차
### Evidence manifest automation
모든 baseline, 통합·precommit 및 readback 검증은 `scripts/evidence_manifest.py`와 `templates/evidence-plan.json`을 사용해 exact argv, exit code, 제한된 output, 허용된 환경, source revision, 파일 hash와 표준 JSON-pointer claims를 기록한다. 명령은 argv 배열만 허용하고 명시된 최소 child-environment allowlist에서 실행하며 output 상한을 넘는 process는 중단한다. Secret·header·private-key·password 값이나 plan 금지 패턴이 발견되면 redaction하지 않고 fail-closed한다. `run`은 baseline부터 최종 precommit까지 사용하고, `validate`는 제품 tree의 evidence exclusion 없이 strict clean checkout에서 PR claim 생성 전에 실행한다. 새 기록은 external verification root의 v2이며 exact current HEAD와 source blob·hash를 다시 검증한다. 과거 evidence-only descendant는 명시적 archived v1 read-only 호환 경로에 한정한다. 수동으로 매칭되지 않는 PR 성공·검증 주장을 만들지 않는다. Comment publish 전 remote immutable artifact의 digest를 authoritative readback으로 비교한다.
### Conditional pytest evidence recipe
pytest를 이미 사용하는 프로젝트의 evidence command에만 적용한다. unittest 예시나 제품의 pytest 설정을 일괄 변경하지 않고, 승인된 interpreter·pytest 버전·plugin 및 기존 test target을 사용한다. 아래 JSON argv의 `{python}`과 `{tests}`를 exact interpreter와 승인 target으로 치환한다. 여러 target은 각각 argv 원소로 명시하고 shell 문자열로 합치지 않는다.
```json
["{python}", "-B", "-m", "pytest", "-o", "addopts=", "--color=no", "-rA", "{tests}"]
```
- 이 recipe는 command-local `addopts=`로 프로젝트의 quiet 누적 옵션을 해제하여 최종 summary를 보존하는 예시다. 기본적으로 `PYTEST_ADDOPTS`는 기존 child-environment allowlist에 없으며 전달하지 않는다. Quiet 자체가 항상 summary를 누락시키는 원인이라고 단정하지 않는다. 프로젝트 `addopts`의 필수 실행·plugin 옵션은 사전에 확인하여 필요한 옵션을 승인된 argv에 명시적으로 옮긴다. 필수 semantics를 보존할 수 없으면 recipe를 그대로 적용하지 않고 `evidence_gap`으로 남긴다. Warning 필터·skip 조건·선택 대상·제품 config 파일은 변경하지 않는다.
- 실제 승인 환경에서 먼저 최종 passed/skipped/failed 및 warning 요약의 보존 여부를 확인한다. pytest가 생략하는 zero category를 출력된 수치처럼 보고하거나 임의로 0으로 채우지 않는다. 이전 실행의 수치나 progress 문자 개수로 이번 실행 합계를 보충하지 않는다. Recipe만으로 모든 pytest 버전·plugin의 output 보존을 보장하지 않는다.
- 기존 evidence plan의 finite `timeout_seconds`, `max_output_bytes`, expected exit code와 process cleanup을 유지하고 exact argv·환경·source SHA·현재 실행 output을 기록한다. `timeout`·`output_limit`·불완전 summary이면 수치 claim은 `evidence_gap`이다. 명령이 완료되고 exit 0인 사실과 정확한 수치 보고의 증거는 구분한다. 승인 없이 재실행하거나 output 한도를 늘리지 않는다.
- 별도의 parser, runner, manifest schema 또는 pytest 설치 의무를 추가하지 않는다. Synthetic regression은 격리된 pytest interpreter에서 실제 pass/skip/warning과 failure 출력 및 기존 bounded reader의 보존을 검증한다. Pytest가 없는 test interpreter에서는 실행 회귀가 명시적으로 skip되므로 문서 계약 테스트만 통과한 결과를 실제 pytest 검증으로 보고하지 않는다. Synthetic test의 plugin autoload 차단은 fixture 격리용이며 제품 실행 정책이 아니다.
### 1. Target, identity와 review 고정
1. Exact target identifier를 검증하고 broker `serve`의 ready 응답과 public `request` API로 Issue lease를 확인한다.
2. Canonical authenticated Gitea adapter로 impl role identity, repository, Issue, PR, commits, changed files, statuses, workflow runs, 전체 reviews와 review comments를 읽는다.
3. 입력 승인 gate를 적용하고 Issue·PR·review snapshot을 저장한다.
4. Review marker에 head, contract digest, result digest 또는 correlation이 있으면 형식과 authoritative 값의 일치를 검증한다. Marker가 없는 code review는 review ID·author·commit binding과 finding body를 그대로 보존한다.
5. Finding matrix를 만들고 `blocked_spec` 또는 판정 불가능한 finding이 있으면 수정 전에 중단한다.
완료 조건: exact review와 reviewed head, current PR head, Issue contract digest와 finding matrix가 동일 snapshot에 결합돼 있다.
### 2. Cache와 기존 worktree 검증
1. `read_file`로 `operations/repository-cache.md`를 새로 읽는다.
2. Issue lease를 유지한 채 같은 broker handle로 `acquire_repository` request를 보내 cache path·ownership·permission·bare·remote를 검증한 뒤 origin에서 prune/fetch한다.
3. Gitea default branch와 local ref, PR base·head object 및 remote head branch SHA를 대조한다. Fork PR이면 target cache에 head object가 있다고 가정하지 않고 authoritative head repository와 branch를 별도로 검증한다.
4. Broker Issue lease와 repository lease를 모두 유지한 상태에서 기존 Issue worktree의 state identity, Git registration, common directory, detached·locked 상태, current HEAD, index, tracked·untracked state와 미완료 Git operation을 검증한다.
5. Worktree HEAD와 PR head SHA가 다르거나 dirty state가 snapshot으로 설명되지 않거나 다른 실행의 lock·Git operation이 있으면 reset·checkout·clean으로 복구하지 않고 중단한다.
6. Worktree admission 검증이 모두 끝난 뒤 `release_repository` request를 보내고 일반 편집·test·commit 동안 Issue lease만 유지한다.
7. 다른 checkout, 임시 clone 또는 경쟁 worktree를 만들지 않는다.
완료 조건: 기존 worktree가 exact owner, repository, Issue, impl role, PR branch와 reviewed head에 결합되고 보존할 모든 기존 상태가 기록돼 있다.
### 3. 재작업 계획과 baseline
1. 각 `valid_fix`와 `evidence_gap`을 승인 계약의 acceptance criterion에 역추적한다.
2. 변경 allowlist를 source, 문서, 설정, test와 evidence path로 만든다. 불변성 또는 evidence 재사용 검증에는 계약을 실제로 소유하는 exact path·제한된 glob만 별도 기록하며 package 전체처럼 넓은 경로를 근거 없이 사용하지 않는다. Review body의 path를 존재 검증 없이 신뢰하지 않는다.
3. Repository의 canonical 문서 README·TEMPLATE, build·test·lint 명령과 변경 대상 파일을 다시 읽는다.
4. 현재 reviewed head에서 관련 test와 review가 요구한 runtime check를 baseline으로 실행하고 command, exit code, output, environment와 HEAD를 기록한다.
5. Framework·runtime·protocol capability가 불명확하면 production edit 전에 최소 spike로 검증한다.
6. Review가 실제 browser 동작을 요구하면 framework test client나 protocol client로 대체하지 않는다. 사용할 수 있으면 Playwright MCP `mcp__playwright__browser_*`로 실제 Chromium 흐름과 browser storage·cookie·navigation을 검증하고, 사용할 수 없으면 해당 criterion을 unresolved로 유지한다.
완료 조건: 각 remediation item에 exact path, 실패 또는 부족한 baseline evidence와 완료를 증명할 검증 방법이 연결돼 있다.
### 4. Test-first 수정
1. `valid_fix`마다 결함을 재현하는 가장 작은 regression test를 먼저 추가하거나 기존 test가 의도한 이유로 실패함을 확인한다.
- Test file과 test function 이름은 추적 번호가 아니라 안정적인 기능·동작·불변조건을 표현한다. Issue, PR, review, comment 번호나 일시적인 finding ID를 이름에 넣지 않는다. 추적성은 finding matrix, commit, PR comment와 evidence metadata에 기록한다. 기존에 이미 존재하는 번호 기반 이름은 해당 재작업에서 별도 rename 범위를 승인받지 않았다면 유지한다.
- 동시성·event-loop finding은 wall-clock 의존을 최소화한 deterministic unit test와 실제 delay를 활성화한 짧은 runtime probe를 모두 실행해, 지연 semantics 유지와 무관한 coroutine·health 요청의 응답성을 각각 검증한다.
- Streaming request-body 상한 finding은 `Content-Length`를 신뢰하지 않고 실제 streamed bytes로 exact limit 허용, limit+1 거부, 누락·부정확한 `Content-Length`, 초과 뒤 `receive` 미호출을 검증한다. Upstream이 이미 전달한 단일 대형 chunk의 allocation은 애플리케이션이 막을 수 없다는 잔여 경계를 기록한다.
2. Source를 최소 변경해 test를 통과시키고 이미 satisfied인 criterion과 제외 범위를 보존한다.
3. `evidence_gap`은 production code를 불필요하게 바꾸지 않고 누락된 unit, integration, runtime, browser 또는 provenance evidence를 보강한다.
4. 보안 finding은 정상 경로뿐 아니라 fail-closed, no-call·no-write, 경계 시간, rotation 양방향, fresh session, logout 후 재접속과 credential 비노출을 해당 계약 범위에서 검증한다.
5. 문서 finding은 기존 canonical requirement ID와 revision history를 유지하고 요구사항, interface, project structure, runtime flow, deployment와 test를 일관되게 갱신한다. Review를 맞추기 위해 별도 요구사항 ID를 만들지 않는다.
6. 실제 credential, 운영 데이터 또는 민감 token을 fixture, evidence, log, screenshot, URL이나 repository file에 기록하지 않는다.
7. 각 item을 수정할 때 focused test를 실행하고 matrix의 `result`와 evidence를 갱신한다.
완료 조건: 모든 `valid_fix`가 재현 test와 수정 후 통과 evidence를 가지며, `evidence_gap`은 요구된 실제 검증 수준으로 채워졌거나 unresolved 이유가 명시돼 있다.
### 5. 통합 검증과 독립 검토
1. Repository가 선언한 formatter, lint, type check, build, targeted, integration과 relevant full suite를 실행한다.
2. Runtime·protocol·browser criterion은 가능한 가장 실제에 가까운 경로로 실행한다. 하위 수준 대체 evidence를 동등하다고 주장하지 않는다.
재작업 diff가 문서 파일에만 한정되고 기존 browser evidence가 결합된 source, test와 evidence path가 reviewed head부터 final head까지 byte-identical이며 ancestry를 검증한 경우에만 browser를 다시 실행하지 않고 기존 evidence를 재사용할 수 있다. 이때 비교한 exact path 집합과 양 head의 blob OID 또는 digest를 검증 기록과 결과 comment에 남긴다. 변경 경로가 겹치거나 동일성을 증명할 수 없으면 재사용하지 않는다.
3. Base-to-worktree와 reviewed-head-to-worktree diff를 모두 읽고 모든 changed file을 finding matrix와 acceptance matrix에 대응한다.
4. 범위 밖 refactor, debug artifact, generated drift, credential, 운영 데이터, test-only bypass와 예상하지 못한 삭제를 제거한다.
5. 독립 검토를 요청하기 전에 모든 actionable finding을 하나의 통합 adversarial suite로 실행한다. 각 finding은 실제 public response shape와 모든 직접 소비 경로를 포함해야 한다. 최소 검증 항목은 최초 실행과 idempotent replay의 응답 동등성, top-level/nested 필드 일치, 재귀적 민감정보 redaction, missing/initialized DB의 no-write, public positional signature, 동시성 및 실패 envelope다. 이 suite가 통과하지 않으면 독립 검토를 시작하지 않는다. 독립 검토는 새 결함을 처음 찾는 단계가 아니라 exact tree의 확인 단계다.
6. Security-sensitive 변경은 `delegate_task`로 bounded reviewer에게 exact worktree, reviewed head, contract subset, finding matrix와 file allowlist를 제공해 독립 검토한다. Gitea write, Git cleanup, commit과 push는 금지한다.
7. Delegate finding을 main agent가 직접 재현하고 유효한 문제만 수정한 뒤 영향을 받은 focused test와 relevant full suite를 다시 실행한다.
완료 조건: 모든 modified file이 approved finding 또는 필수 supporting change로 설명되고 unresolved finding과 검증 한계가 분리돼 있다.
### 6. Drift 확인과 commit
1. Issue, comments, PR, commits, changed files, branch SHA와 전체 reviews를 다시 읽는다.
2. Issue body digest·`updated_at`, PR base·head, 대상 review binding, review baseline 또는 default branch가 snapshot에서 material하게 drift하면 새 상태를 자동 채택하지 않고 중단한다.
3. Worktree의 staged·unstaged·untracked state와 미완료 Git operation을 확인한다.
4. Contract Preservation Inventory가 필요한 경우 final tree에 다시 대응해 baseline 항목 수와 실제 상태를 검증하고 `missing=0`, `ambiguous=0`이 아니면 stage하거나 commit하지 않는다.
5. Exact allowlist만 stage하고 staged diff를 다시 읽는다.
6. Commit 전에 subject와 body를 공통 [commit-message reference safety](../../../../../operations/repository-cache.md#commit-message-reference-safety) 규칙에 따라 확인한다. Protected Issue metadata가 immutable이어야 하면 hash-number auto-reference와 parser-triggering closing/reopening 문구를 피하고 `Issue 18 / PR 19 / review 76` 또는 evidence로 추적한다. 기본은 새 non-amended commit이며, amend는 명시적 사용자 승인·미게시 증명·tree 동일성 증명과 새 SHA exact verification이 모두 있을 때만 허용된다. 일반 amend 권한으로 해석하지 않는다.
7. Commit 후 clean HEAD에서 최종 formatter, test와 필요한 runtime·browser 검증을 다시 실행한다. 이 뒤 repository file을 바꾸면 영향을 받은 검증을 다시 실행한다.
8. 최종 검증 기록은 exact HEAD 또는 검증한 source tree, command, exit code, pass·skip·fail 수치, 환경, relevant output과 reviewer가 접근 가능한 evidence location을 포함하며, PR body·결과 comment의 각 실행 주장과 수치는 해당 location에 일대일 대응시킨다. Hermes 내부 tool transcript, delegate 자기보고, 삭제 예정 임시 파일, reviewer가 접근할 수 없는 local path와 요약 수치만 있는 기록은 evidence로 인정하지 않고 `evidence_gap` 또는 `partial`로 유지한다. 새 evidence는 checkout 밖 정본 verification root에 보존하여 final HEAD를 기록하며 evidence-only descendant를 만들지 않는다. 과거 tracked evidence의 archived 분석은 fresh 실행·원격 head 검증과 구분한다.
완료 조건: final local HEAD, clean state, finding별 해결 evidence와 단일 검증 기록이 일치하며, 필요한 Contract Preservation Inventory는 `missing=0`, `ambiguous=0`이다.
### 7. 기존 PR branch 전달
각 mutation은 [operation API contract](../../../OPERATIONS.md)의 `execute_rework_operation`으로 처리한다. Exact 기존 comment 재사용은 `verify_rework_reuse`의 fresh read-only 경로다. 아래 기존 보호 계약과 각 실제 readback verifier는 그대로 적용하며 coordinator 결과만으로 handoff를 판정하지 않는다.
1. Write 직전에 remote branch와 PR head가 snapshot의 reviewed head와 여전히 exact-match하는지 확인한다.
2. 첫 delivery push 전에 승인된 noninteractive credential capability를 preflight하고 공통 [non-file Git credential helper](../../../../../operations/repository-cache.md#non-file-git-credential-helper)를 role-owned short-lived child process에서 사용한다. Credential은 Git의 private protocol pipe로만 공급하고 출력·파일·config에 저장하지 않으며 interactive/다른 helper fallback을 비활성화한다. 성공·실패와 무관하게 child process를 wait/reap하여 credential 환경의 종료를 확인한다. 그 후 canonical mirror remote를 변경하지 않고 explicit repository URL과 refspec으로 기존 Issue 전용 branch에 fast-forward non-force push한다. Named mirror remote를 사용해야 하면 `operations/repository-cache.md`에 따라 해당 push 호출에서만 `remote.<name>.mirror=false`를 적용한다.
3. Push 결과만 신뢰하지 않고 remote branch SHA와 PR head SHA가 final local HEAD와 exact-match하는지 readback한다.
4. PR body 갱신이 필요한 경우 write 직전에 PR 전체 필드, review·comment·commit collection cardinality와 authenticated impl identity를 다시 읽는다. Current body의 `Closes #<issue-number>`와 기존 구현 내용을 보존하고 재작업 review ID·reviewed head·finding별 조치·최종 검증과 unresolved limitation만 반영한 exact body payload를 명시해 한 번만 호출한다. 성공 응답만 신뢰하지 않고 fresh PR과 관련 collection을 읽어 PR author stable account ID, exact body payload, `updated_at`, base·head·commit/ref, state·draft·merge 상태, labels·assignees·reviewer와 collection cardinality를 baseline과 비교한다. Exact body와 provider가 해당 body update로 파생한 `updated_at` 외의 변경은 승인되지 않았으며 인과를 증명할 수 없으면 partial로 중단한다.
5. PR changed files, commits, state, merge 상태, statuses와 workflow runs를 다시 읽는다. Local test, commit status와 Actions run을 별도 사실로 기록한다.
6. Finding matrix와 최종 검증 기록을 bounded JSON input으로 만들고 `scripts/rework_comment.py render --input <input.json> --comment <comment.md> --expected <expected.json> --delegation-adoption <absolute-main-declaration.json>`를 실행해 canonical 재작업 결과 comment, marker-free digest와 correlation을 생성·검증한다. Comment body와 marker를 수동 조립하지 않는다.
7. Exact PR top-level comment endpoint target과 fresh PR metadata binding을 검증해 Source Issue comments, inline comments, review replies와 native reviews에서 분리하고 scoped collection을 complete pagination한 뒤 canonical deduplication 규칙을 적용한다. Existing exact comment가 없으면 exact PR conversation에 body를 한 번 게시하고, 있으면 write 없이 재사용한다.
8. Exact comment와 scoped complete collection을 fresh normalized JSON으로 readback한 뒤 `scripts/rework_comment.py verify --expected <expected.json> --pr-readback <pr.json> --comments-readback <comments.json> --identity-readback <identity.json> --action <create|reuse> --baseline-count <N> --output <handoff-receipt.json>`를 실행한다. Receipt의 comment ID, impl author stable account ID, body, timestamp, final head, digest, correlation과 cardinality가 검증돼야 하며, 신규 게시로 허용된 PR-underlying-issue projection 외의 PR field, Source Issue field와 기존 review collection에 변경이 없어야 한다.
9. Native review를 작성·dismiss·reply하거나 새 review를 요청하지 않는다. 기존 `REQUEST_CHANGES`는 reviewer가 새 head를 재검토하기 전까지 자동 해소된 것으로 주장하지 않는다.
10. Push, PR body 또는 comment write가 timeout·오류이면 blind retry하지 않고 branch, PR과 complete comment collection을 authoritative하게 reconcile한다. Exact write가 반영됐음을 증명할 수 없으면 partial 상태로 중단한다.
완료 조건: 같은 PR의 remote head가 final local HEAD와 일치하고 canonical 재작업 결과 comment가 정확히 하나 readback됐으며, `gitea-rework-handoff/v1` receipt가 `status=verified`, `readback_verified=true`로 생성되고 모든 authorized external write와 후속 review 필요성이 명확하다. Receipt가 없거나 검증에 실패하면 lock은 정상 해제하되 결과를 `partial|failed`로 보고하며 `completed`를 주장하지 않는다.
### 8. 종료와 다음 review handoff
1. 임시 검증 자원을 정리하기 전에 relevant output을 redaction하고 exact HEAD·command·exit code·환경과 결합해 reviewer-accessible evidence location에 저장한 뒤 그 내용을 readback한다. 그 후 임시 server, probe script·raw output, browser context, fixture workspace와 credential helper를 정리하고 부재를 확인한다.
2. 현재 실행이 시작한 tracked broker process와 그 자식 process를 cleanup 대상으로 관리한다. Shell PID 종료만으로 해제를 판정하지 않으며, 보유한 repository lease를 `release_repository`하고 Issue lease를 `release`한 뒤 broker를 wait/reap하고 `probe`에서 `free=true`를 확인한 경우에만 cleanup 완료로 판정한다.
3. Finding matrix, final diff, 검증 기록, remote head와 CI/status를 다시 대조한다.
4. 모든 item이 `resolved`여도 reviewer의 새 head 검토 전에는 구현 승인 완료가 아니라 `재작업 전달 및 comment handoff 완료, 재검토 대기`로 보고한다.
5. 후속 review가 새 head에 `REQUEST_CHANGES`를 남기면 그 review를 새 immutable snapshot으로 하는 별도 재작업 iteration을 시작한다. 이전 review snapshot이나 matrix를 새 head에 그대로 재사용하지 않는다.
완료 조건: resource와 lock 정리가 검증되고, 재작업 결과가 exact final head와 다음 review 입력으로 전달 가능하다.
## 위임 규칙
- Main impl-agent가 contract 해석, finding 분류, drift 판정, 최종 diff, Git·Gitea write와 delivery에 책임을 진다.
- 독립적인 source 조사, test 작성, disjoint 모듈 수정, browser evidence 수집, 실패 분석과 security review는 bounded delegate에 위임할 수 있다.
- Delegate에는 exact worktree path, reviewed head, contract subset, finding IDs, file allowlist와 검증 target을 제공한다.
- 같은 파일을 병렬 worker에게 주지 않고 Gitea write, lock·worktree 변경, commit, push, reset과 cleanup을 금지한다.
- Delegate summary를 evidence로 사용하지 않고 main agent가 diff, command output, browser 결과와 artifact를 직접 검증한다.
- 외부 side effect와 credential 사용은 main agent만 수행하고 즉시 authoritative readback한다.
## 금지 사항
- Review finding을 승인 계약보다 우선하거나 새 요구사항으로 승격하지 않는다.
- `blocked_spec`을 임의 설계 선택으로 해결하지 않는다.
- 이전 head의 stale review를 current head의 재작업 근거로 자동 적용하지 않는다.
- 기존 dirty worktree를 reset, clean, stash, checkout, switch, rebase 또는 삭제하지 않는다.
- Review를 해결한다는 이유로 already-satisfied 동작, security gate, validation 또는 실패 경로를 약화하지 않는다.
- Unit test, framework AppTest, HTTP client 또는 WebSocket client를 실제 browser evidence로 표현하지 않는다.
- PR body의 성공 주장, process exit code, delegate 보고 또는 push 응답만으로 해결을 주장하지 않는다.
- Canonical 재작업 결과 comment 이외의 PR comment, inline comment 또는 review reply를 작성하지 않는다.
- Existing `REQUEST_CHANGES` state를 직접 dismiss하거나 reviewer 대신 승인 완료로 판정하지 않는다.
- 새 PR, 새 branch, merge, Issue 변경, release 또는 deployment를 수행하지 않는다.
## 검증 및 보고
완료 보고에는 다음을 구분해 포함한다.
- authenticated impl identity와 exact repository, Issue, PR
- Issue contract `updated_at`, body digest와 review가 참조한 contract digest
- 대상 review ID·author·state·reviewed head·stale 상태와 current pre-rework head
- Finding별 classification, 계약 근거, 수정 path, test와 `resolved|unresolved` 결과
- Worktree path·identity, reviewed-head 시작 상태와 final local HEAD
- Baseline, focused, full-suite, runtime과 browser 검증의 exact command·exit code·결과
- 실제 browser가 요구된 경우 사용한 browser runner와 발급·복원·이동·삭제 등 관찰 결과. 기존 evidence를 재사용했다면 비교한 exact path 집합과 reviewed/final head의 blob OID 또는 digest
- Base 및 reviewed head 대비 전체 modified file과 범위 근거
- Local commit, remote branch와 PR final head SHA readback
- PR body write 여부, status와 Actions run의 success·failure·pending·없음 상태
- 재작업 결과 comment ID·URL·author·result·digest·correlation, exact final head binding과 verified handoff receipt
- 기존 review가 유지됨과 새 head의 후속 review 필요 여부
- 수행하지 않은 review·merge·Issue·release·deployment write
- Baseline failure, unresolved finding, specification blocker, unavailable evidence와 partial write
- 임시 resource 정리와 lock 해제 확인
모든 finding이 해결되고 검증된 final commit과 canonical 결과 comment가 기존 PR에 전달돼도 후속 reviewer가 새 head를 검토하기 전에는 `승인 완료`라고 보고하지 않는다. Final head와 exact comment readback, 전체 diff, 실제 검증 evidence와 resource·lock 정리가 일치할 때만 `재작업 전달 및 comment handoff 완료`로 판정한다.
## Canonical external verification evidence
검증 실행 기록은 공통 [Repository cache 운영 지침](../../../../../operations/repository-cache.md)의 정본 경로 `<harness-root>/repositories/<owner>/<repo>/worktrees/issue-<number>/verification/<run-id>`에 보존한다. Product `checkout`과 artifact root를 분리하며, 회차별 evidence를 제품 `docs/verification/`에 새로 커밋하지 않는다. 기존 정본 문서·테스트·fixture·추적 파일은 자동 이동·삭제·ignore하지 않는다.
인증된 Issue/cache/worktree admission을 마친 caller가 broker Issue lease 아래 evidence `run` 전에 live lease의 exact harness root·Issue identity를 인증하고 root를 준비한다. Stage별 새 run-id를 사용한다. 아래 prepare는 `verification/`과 `<run-id>/`를 각각 별도 생성하고 각 directory를 실행 UID 소유 mode `0700`으로 만들며 즉시 `lstat`한다. Symlink, owner·mode 불일치와 collision은 거부하고 기존 malformed root를 자동 chmod하거나 repair하지 않는다. 이 경로에는 중간 parent가 umask를 따르는 `Path.mkdir(parents=True, mode=0o700)`를 사용하지 않는다. Helper는 lease/cache를 획득·복구하지 않는다. `<harness-root>`는 실제로 읽은 contract path로 고정하고 다음 변수는 그 root와 인증된 Issue/run identity에서 결정한다. 아래 명령은 harness root에서 호출한다.
```sh
python3 harness/lib/verification_root.py prepare \
--root "$HARNESS_ROOT" --owner "$OWNER" --repository "$REPOSITORY" \
--issue "$ISSUE" --run-id "$RUN_ID" --handle "$HANDLE"
python3 harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py run \
--contract-path "$HARNESS_ROOT/harness/operations/repository-cache.md" \
--repo-root "$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/checkout" \
--artifact-root "$VERIFICATION_RUN_ROOT" \
--plan evidence-plan.json --output evidence-manifest.json
python3 harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py validate \
--contract-path "$HARNESS_ROOT/harness/operations/repository-cache.md" \
--repo-root "$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/checkout" \
--artifact-root "$VERIFICATION_RUN_ROOT" \
--manifest evidence-manifest.json
```
`VERIFICATION_RUN_ROOT`는 `$HARNESS_ROOT/repositories/$OWNER/$REPOSITORY/worktrees/issue-$ISSUE/verification/$RUN_ID`의 exact 절대 경로다. Plan의 `hash_paths`만 repository-relative이고 plan/output/manifest는 run-relative다. Source filename과 artifact filename이 같아도 같은 파일로 취급하지 않는다. `run`은 **strict clean checkout**, evidence **exclusion 없음**, full SHA·source/plan digest와 실제 command/exit code/bounded output binding을 요구하고 `evidence-manifest/v3`를 게시한다. Existing manifest를 덮어쓰지 않는다. `validate`도 같은 roots와 exact current HEAD를 요구한다. Dirty 개발 tree에서 validation 성공을 주장하지 않으며, final tracked 산출물을 commit한 뒤 그 exact commit을 검증하고 실패하면 push하지 않는다.
PR/결과 보고에는 stage/run-id, source SHA, manifest·plan digest, 실제 command/result/limitations와 reviewer 접근 수단을 함께 기록한다. Same-UID reviewer는 기록만 read-only로 읽고, distinct-UID/host이면 승인된 credential-free read-only export와 digest/readback receipt를 제공한다. 경로만 제시하거나 권한을 완화하는 것은 접근 검증이 아니다. 확인할 수 없으면 `evidence_gap`으로 보고한다. Product 테스트 증거와 harness 자체 검증을 구분하고, 인증정보 값은 `[REDACTED]`로만 기록한다.
Remote branch/PR head를 authoritative하게 읽어 source SHA·exact artifact와 결합하고 cleanup 전 실제 record readback을 확인한다. Helper의 local validate가 Gitea readback을 대신하지 않는다. Checkout 제거와 verification 폐기는 분리하며 완료 기록은 보존한다. 기존 v1은 `validate-legacy`의 read-only 호환 경로만 사용하고 새 publication proof로 사용하지 않는다.
### Tracked bounded background evidence execution
Full suite가 포함되어 foreground tool 시간창을 넘을 것으로 예상되는 evidence plan은 기존 실행 도구의 **tracked background** 작업으로 시작한다. 새 scheduler나 process manager를 만들지 않는다. 실행 직전에 exact run-id, source SHA, plan digest, output path와 command를 기록하고 시작 응답의 process/session handle을 결합해 보존한다. Handle이나 PID만으로 source/admission 또는 실행 성공을 판정하지 않는다. Broker Issue lease와 기존 admission 조건은 그대로 유지하며, lease를 유지·검증할 수 없으면 외부 handoff를 차단한다.
Background도 각 command의 `timeout_seconds`를 유지하고, main이 plan의 command deadline·정리 예산·실행 overhead를 고려해 유한한 outer deadline을 명시한다. 기존 도구의 tracked wait/poll·완료 통지를 사용하되 outer deadline 만료 시에는 기존 bounded process-group cleanup과 wait/reap 절차를 적용한다. Notification이나 manifest 파일의 존재만으로 완료를 선언하지 않는다. 실행 도구가 필요한 추적·deadline·정리 capability를 제공하지 않으면 시작하지 않고 blocker를 보고한다. Background가 세션 종료 이후의 지속성을 보장하지 않는다.
Foreground 응답 timeout이나 통지 누락은 실제 evidence process 종료 또는 제품 assertion 실패의 증거가 아니다. 자동 retry하지 않는다. 재실행 판단 전에 보존한 handle로 기존 실행의 상태·실제 exit code·process 종료와 cleanup을 확인하고, 같은 run-id의 manifest를 exact source SHA·plan digest·output path에 결합해 canonical `validate`로 확인한다. Validation은 process 종료·cleanup 확인을 대신하지 않는다. Tool 관측과 artifact가 상충하거나 실행 중·상태 불명·cleanup 미확인이면 `pending/partial`로 유지하고 중복 실행하지 않는다. 같은 실행의 상태 확인과 기록 정리만 계속하며 완료된 suite를 다시 수행하지 않는다.
종료·cleanup이 확인된 성공은 기존 evidence를 검증해 재사용한다. 실패는 실제 exit code와 bounded 부분 출력·termination 기록을 보존한다. Timeout·출력 초과·cleanup 실패를 TDD RED로 보고하지 않는다. 실패한 manifest가 `validate`를 통과하지 못하더라도 그 자체를 재실행 허가로 삼지 않는다. 기존 실행이 종료되고 cleanup이 확인된 실패에 대해서만 별도 승인 후 새 run-id로 실행한다. 기존 artifact를 덮어쓰지 않는다. 이 절차는 workflow 문서 계약이며 evidence runner/schema, lease/TTL, publication journal 및 자동 retry 정책을 변경하지 않는다.
Command deadline 계약: 새 plan의 각 command는 `timeout_seconds`를 명시한다. Boolean·문자열·누락·비유한 값·0 이하·3600 초 초과를 실행 전에 거절하며 암묵적인 무제한 실행을 허용하지 않는다. Monotonic wall-clock deadline은 무출력 대기, stdout EOF 이후 실행, leader 종료 뒤 descendant가 유지한 pipe까지 적용한다. 각 command는 별도 session/process group에서 실행하고 timeout·출력 초과·예외 시 해당 process group을 TERM → 제한된 grace → KILL로 정리하고 direct child를 bounded wait로 회수한다. Leader가 종료됐어도 같은 group의 살아 있는 descendant를 남기지 않는다. 이것은 sandbox가 아니며 별도 session으로 이탈한 descendant의 containment나 grandchild 직접 회수를 보장하지 않는다.
`evidence-manifest/v3`는 실제 timeout, `execution_status`, `termination`의 `term_sent`·`kill_sent`·`direct_child_reaped`·`group_cleanup_completed` boolean 기록과 안전한 bounded 부분 출력을 결합한다. 새 claim pointer는 기존 6개에 timeout·실행 상태·종료 기록을 추가한 9개다. Linux `/proc`로 같은 group의 non-zombie 프로세스를 확인하며 확인·정리에 실패하면 `cleanup_failed`로 차단한다. TERM grace는 0.25 초, KILL 후 group 종료·direct-child 회수 예산은 0.5 초이며 오류 fallback wait도 0.1 초로 제한한다. 새 command의 `max_output_bytes`는 0–1048576 범위의 non-boolean 정수만 허용한다. `execution_status`는 `completed|timeout|output_limit|cleanup_failed|descendant_cleanup`이다. 정상 종료 leader 뒤의 살아 있는 descendant를 신호로 정리한 경우도 `descendant_cleanup` 실패이며, `completed`에는 TERM/KILL 신호가 있을 수 없다. Timeout·출력 한도 초과·정리 실패는 expected exit code와 같아도 성공이 아니며 **TDD RED** 또는 제품 assertion 실패로 분류하지 않는다. 해당 실행은 실패로 보존하고 뒤의 command를 실행하지 않으며, 실행된 prefix의 claim만 기록한다. Partial output에도 기존 secret 탐지를 적용하고 인증정보 유출은 fail-closed한다. 자동 재실행은 하지 않으며 새로 승인된 실행에는 새 run-id를 사용한다.
기존 `evidence-manifest/v2`는 명시적 `validate-archived-v2 --contract-path <canonical-contract> --repo-root <checkout> --artifact-root <canonical-run> --manifest <run-relative-manifest>`로 read-only 호환 검증한다. Canonical identity, exact current HEAD, source/plan/output binding을 유지하지만 과거 기록에 없던 timeout이나 종료 보장을 추론하지 않는다. 새 `run`/`validate`에는 v2 fallback이 없다. v1은 기존 `validate-legacy`만 사용한다. 두 archive 경로의 성공은 새 실행·TDD RED·fresh publication proof가 아니며 기존 artifact를 수정하거나 새 schema로 자동 변환하지 않는다.
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""Pure input-bound verifier for the narrowly approved comment correction."""
from __future__ import annotations
import argparse, hashlib, importlib.util, json, re, sys
from pathlib import Path
class Bad(ValueError): pass
def canonical(v):
return json.dumps(v,sort_keys=True,ensure_ascii=False,allow_nan=False,separators=(',',':')).encode()
def sha(b): return hashlib.sha256(b).hexdigest()
def load(path, digest):
directory = str(Path(__file__).resolve().parent)
if directory not in sys.path:
sys.path.insert(0, directory)
from rework_readback_cli import read_bound_json
try:
return read_bound_json(path, expected_sha256=digest)
except Exception:
raise Bad('bounded input or digest rejected') from None
def core_module():
p=Path(__file__).with_name('rework_comment.py')
spec=importlib.util.spec_from_file_location('correction_comment_core',p)
if not spec or not spec.loader: raise Bad('canonical planner unavailable')
m=importlib.util.module_from_spec(spec); sys.modules[spec.name]=m; spec.loader.exec_module(m); return m
def verify_correction(plan, before, after, old_comment, new_comment, approval, *, approval_sha256, historical_failure_sha256, profile='strict'):
if type(approval) is not dict or sha(canonical(approval))!=approval_sha256: raise Bad('approval digest binding')
if not re.fullmatch('[0-9a-f]{64}',historical_failure_sha256 or ''): raise Bad('historical failure digest required')
if approval.get('historical_failure_baseline_sha256')!=historical_failure_sha256 or approval.get('approved') is not True or approval.get('operation')!='same_comment_correction': raise Bad('correction approval mismatch')
if profile not in ('strict','observed-comment-ref-neutralization'): raise Bad('unsupported profile')
if approval.get('timeline_profile') != profile: raise Bad('timeline profile not approved')
if approval.get('ordinary_patch') is not True: raise Bad('ordinary PATCH approval required')
if type(plan) is not dict or plan.get('schema')!='gitea-rework-correction-plan/v1' or plan.get('approval_required') is not True: raise Bad('invalid correction plan')
if plan.get('prior_handoff_verified') is not False or plan.get('external_write_performed') is not False: raise Bad('historical failure cannot be promoted')
# Both snapshots must bind the same expected render and pass the canonical provider normalizer.
if type(before) is not dict or type(after) is not dict: raise Bad('snapshot object required')
if before.get('schema')!='gitea-agent.rework-readback/v1' or after.get('schema')!=before.get('schema'): raise Bad('snapshot schema')
target=before.get('target')
if target!=after.get('target') or before.get('provider')!=after.get('provider') or before.get('actor_id')!=after.get('actor_id'): raise Bad('snapshot identity drift')
expected=approval.get('new_expected_document'); previous=approval.get('old_expected_document')
if type(expected) is not dict or type(previous) is not dict: raise Bad('approval must bind both expected documents')
planner=core_module()
reconstructed=planner.prepare_correction(previous,expected,comment_id=plan.get('comment_id'),actor_id=plan.get('actor_id'))
if reconstructed!=plan: raise Bad('plan does not reconstruct from canonical planner')
try:
rb=Path(__file__).with_name('rework_readback.py'); s=importlib.util.spec_from_file_location('correction_readback_core',rb); m=importlib.util.module_from_spec(s); sys.modules[s.name]=m; s.loader.exec_module(m)
adapter=m.ReworkReadbackAdapter(lambda _:None,origin=m.ORIGIN,credential_source=m.CREDENTIAL_SOURCE,actor_id=before['actor_id'],provider_contract=before['provider'])
bound_before = previous if before.get('expected_sha256') == m.digest(previous) else expected
raw_before=adapter.validate_snapshot(bound_before,before); raw_after=adapter.validate_snapshot(expected,after)
if approval.get('provider_version') != before['provider']['version']: raise Bad('provider version not approved')
if profile != 'strict' and before['provider']['version'] != '1.27.3': raise Bad('unsupported reference profile version')
except Exception as e: raise Bad('provider snapshot validation failed') from e
if old_comment!=plan['old_body'] or new_comment!=plan['new_body']: raise Bad('exact comment bodies mismatch')
cid=plan['comment_id']; aid=plan['actor_id']
if aid != before['actor_id']: raise Bad('plan actor mismatch')
for collected in (raw_before['comments']['rows'], raw_after['comments']['rows']):
if len([row for row in collected if plan['correlation'] in row['body']]) != 1: raise Bad('correlation cardinality')
def comment_match(c,b):
return type(c) is dict and c.get('id')==cid and c.get('body')==b and type(c.get('user')) is dict and c['user'].get('id')==aid and c['user'].get('login')=='gitea-impl-agent'
olds=[x for x in raw_before['comments']['rows'] if x.get('id')==cid]; news=[x for x in raw_after['comments']['rows'] if x.get('id')==cid]
if len(olds)!=1 or len(news)!=1 or not comment_match(olds[0],old_comment) or not comment_match(news[0],new_comment): raise Bad('target comment/actor cardinality mismatch')
if m.timestamp(news[0]['updated_at']) < m.timestamp(olds[0]['updated_at']): raise Bad('comment update timestamp regressed')
for field in ('id','html_url','pull_request_url','issue_url','user','created_at','assets'):
if olds[0].get(field)!=news[0].get(field): raise Bad('comment identity metadata changed')
# Every non-target top-level comment is immutable; target metadata is exact except body/update time.
old_rows={r['id']:r for r in raw_before['comments']['rows']}; new_rows={r['id']:r for r in raw_after['comments']['rows']}
if set(old_rows)!=set(new_rows): raise Bad('top-level comment collection changed')
for ident, row in old_rows.items():
updated=new_rows[ident]
if ident!=cid and row!=updated: raise Bad('unrelated top-level comment changed')
if ident==cid:
left=dict(row); right=dict(updated)
left.pop('body',None); right.pop('body',None); left.pop('updated_at',None); right.pop('updated_at',None)
if left!=right: raise Bad('target comment metadata changed')
for name in ('identity','version','repository','source_issue','source_comments','pr','pr_issue','reviews','inline'):
x,y=raw_before[name],raw_after[name]
if x!=y: raise Bad('protected source or collection changed: '+name)
# Timeline is separately supplied by snapshots at top level or within raw; no lossy masks accepted.
tb=before.get('source_issue_timeline',raw_before.get('source_issue_timeline')); ta=after.get('source_issue_timeline',raw_after.get('source_issue_timeline'))
if (type(tb) is not dict or type(ta) is not dict or tb.get('complete') is not True or ta.get('complete') is not True
or type(tb.get('rows')) is not list or type(ta.get('rows')) is not list): raise Bad('complete timeline collection required')
for timeline in (tb,ta):
pages=timeline.get('pages')
if type(pages) is not list or not pages or any(type(p) is not dict for p in pages): raise Bad('timeline pagination metadata required')
if any(type(p.get('count')) is not int or p['count']<0 for p in pages) or sum(p['count'] for p in pages)!=len(timeline['rows']): raise Bad('timeline pagination incomplete')
headers=pages[-1].get('headers')
if type(headers) is not dict or headers.get('X-Total-Count')!=str(len(timeline['rows'])): raise Bad('timeline declared total mismatch')
for row in tb['rows'] + ta['rows']:
if type(row) is not dict or type(row.get('id')) is not int or row['id'] <= 0: raise Bad('invalid timeline ID')
m.timestamp(row.get('created_at')); m.timestamp(row.get('updated_at'))
br={r.get('id'):r for r in tb['rows'] if type(r) is dict}; ar={r.get('id'):r for r in ta['rows'] if type(r) is dict}
if len(br)!=len(tb['rows']) or len(ar)!=len(ta['rows']) or None in br or None in ar or len(br)!=len(tb['rows']) or len(ar)!=len(ta['rows']): raise Bad('malformed timeline rows')
if set(br)!=set(ar): raise Bad('timeline additions/deletions')
changed=[]
for eid,brow in br.items():
arow=ar[eid]
if brow==arow: continue
if profile!='observed-comment-ref-neutralization' or brow.get('type')!='comment_ref' or brow.get('id')!=eid: raise Bad('timeline change outside approved profile')
immutable=('id','type','html_url','pull_request_url','issue_url','user','created_at','ref_commit_sha','review_id')
if brow.get('user',{}).get('id')!=aid or any(brow.get(k)!=arow.get(k) for k in immutable): raise Bad('comment_ref identity changed')
if brow.get('ref_action')!='none' or arow.get('ref_action')!='neutered': raise Bad('invalid comment_ref transition')
# Only event update time/action and the embedded exact comment projection may move.
allowed={'ref_action','updated_at','ref_comment'}
if {k:v for k,v in brow.items() if k not in allowed} != {k:v for k,v in arow.items() if k not in allowed}: raise Bad('unapproved timeline field mutation')
if brow.get('issue_url') != raw_before['source_issue']['html_url']: raise Bad('reference source Issue mismatch')
if m.timestamp(arow.get('updated_at')) <= m.timestamp(brow.get('updated_at')): raise Bad('reference update timestamp did not advance')
x,y=brow.get('ref_issue'),arow.get('ref_issue')
if type(x) is not dict or type(y) is not dict: raise Bad('missing linked PR projection')
if set(x)!=set(y): raise Bad('linked PR projection fields changed')
for k in x:
if k!='ref_comment' and x[k]!=y[k]: raise Bad('linked PR projection changed')
xc,yc=brow.get('ref_comment'),arow.get('ref_comment')
if x.get('number')!=plan['target']['pr'] or x.get('html_url')!='https://gitea.jsfamily.quest/'+plan['target']['owner']+'/'+plan['target']['repository']+'/pulls/'+str(plan['target']['pr']): raise Bad('comment_ref linked PR mismatch')
if type(xc) is not dict or type(yc) is not dict or xc.get('id')!=cid or yc.get('id')!=cid: raise Bad('linked comment identity mismatch')
if set(xc)!=set(yc): raise Bad('linked comment projection fields changed')
for k in xc:
if k not in ('body','updated_at') and xc[k]!=yc[k]: raise Bad('linked comment projection changed')
if xc != olds[0] or yc != news[0]: raise Bad('timeline body projection mismatch')
changed.append(eid)
if profile=='strict' and changed: raise Bad('strict timeline preservation failed')
if profile!='strict' and len(changed)>1: raise Bad('reference profile cardinality')
bindings = {'plan_sha256':sha(canonical(plan)), 'before_sha256':sha(canonical(before)), 'after_sha256':sha(canonical(after)), 'approval_sha256':approval_sha256}
return {**bindings, 'schema':'gitea-rework-correction-receipt/v1','status':'verified','operation':'same_comment_correction','target':plan['target'],'comment_id':cid,'actor_id':aid,'head':plan['head'],'correlation':plan['correlation'],'old_body_sha256':plan['old_body_sha256'],'new_body_sha256':plan['new_body_sha256'],'historical_failure_baseline_sha256':historical_failure_sha256,'historical_handoff_status':'partial_unchanged','timeline_profile':profile,'changed_timeline_ids':changed,'atomic_client_precondition':False,'assurance_limitations':['input-bound readback comparison; not causal event attestation','no claim of live provider/build equivalence','ordinary PATCH residual race remains unless separately evidenced']}
def main():
p=argparse.ArgumentParser(); sp=p.add_subparsers(dest='cmd',required=True); v=sp.add_parser('verify')
for n in ('plan','before','after','old-comment','new-comment','approval'): v.add_argument('--'+n,required=True); v.add_argument('--'+n+'-sha256',required=True)
v.add_argument('--historical-failure-sha256',required=True); v.add_argument('--profile',choices=('strict','observed-comment-ref-neutralization'),default='strict'); v.add_argument('--output',required=True)
a=p.parse_args()
try:
from rework_readback_cli import OutputFile
paths=[Path(getattr(a,n.replace('-','_'))) for n in ('plan','before','after','old_comment','new_comment','approval')]
out=Path(a.output)
if any(out==x or out.resolve()==x.resolve() for x in paths): raise Bad('output aliases input')
vals={n:load(getattr(a,n.replace('-','_')),getattr(a,n.replace('-','_')+'_sha256')) for n in ('plan','before','after','old_comment','new_comment','approval')}
if 'source_snapshot' in vals['before']:
envelope=vals['before']; vals['before']=dict(envelope['source_snapshot'],source_issue_timeline=envelope.get('source_issue_timeline'))
if envelope.get('plan') != vals['plan']: raise Bad('envelope plan mismatch')
if 'source_snapshot' in vals['after']:
envelope=vals['after']; vals['after']=dict(envelope['source_snapshot'],source_issue_timeline=envelope.get('source_issue_timeline'))
approval_digest=sha(canonical(vals['approval']))
receipt=verify_correction(vals['plan'],vals['before'],vals['after'],vals['old_comment'].get('body'),vals['new_comment'].get('body'),vals['approval'],approval_sha256=approval_digest,historical_failure_sha256=a.historical_failure_sha256,profile=a.profile)
with OutputFile(out,initial_only=True) as f: f.write(receipt)
except Exception as e: print('error: '+str(e),file=sys.stderr); return 2
return 0
if __name__=='__main__': raise SystemExit(main())
@@ -0,0 +1,136 @@
"""Main-owned, bounded publication operations for implementation rework.
Adapters are trusted provider-specific contracts. This module deliberately
contains no transport code; it validates operation payloads and delegates one
approved attempt to the shared process-local coordinator.
"""
from __future__ import annotations
import copy
import sys
from pathlib import Path
_ROOT = Path(__file__).resolve().parents[6]
if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT))
from harness.lib import impl_operation as _ops
from harness.lib.delegation_adoption import require_adoption
_SCRIPTS = str(Path(__file__).resolve().parent)
if _SCRIPTS not in sys.path:
sys.path.insert(0, _SCRIPTS)
from rework_comment import _correction_expected, prepare_correction
OPERATIONS = frozenset(("push", "pr.patch", "comment.create", "correction"))
def _payload(intent):
p = intent.payload
if type(p) is not dict:
raise ValueError("operation payload must be an object")
return p
def _validate(intent, approval):
_ops.validate(intent, approval)
op = intent.operation
if op not in OPERATIONS:
raise ValueError("operation is not a rework publication operation")
p = _payload(intent)
if op == "push":
if type(p.get("head")) is not str or p["head"] != intent.source_sha:
raise ValueError("push requires explicit intended full head")
elif op == "pr.patch":
if type(p.get("body")) is not str or not p["body"] or len(p["body"].encode()) > 1_000_000:
raise ValueError("bounded PR body required")
if "closing" not in p or type(p["closing"]) is not str:
raise ValueError("adapter contract must bind intended closing")
if p["closing"] not in p["body"]:
raise ValueError("PR body does not preserve intended closing")
elif op == "comment.create":
expected = p.get("expected")
canonical = _correction_expected(expected, legacy=False)
if canonical["head"] != intent.source_sha:
raise ValueError("comment head differs from intended source")
if intent.target != canonical["owner"] + "/" + canonical["repository"] or intent.target_id != int(canonical["pr"]):
raise ValueError("comment intent scope mismatch")
target = p.get("target")
if target != {"owner": canonical["owner"], "repository": canonical["repository"],
"issue": int(canonical["issue"]), "pr": int(canonical["pr"])}:
raise ValueError("comment target differs from canonical expected target")
else:
required = {"old_expected", "new_expected", "comment_id", "actor_id"}
if set(p) != required:
raise ValueError("correction payload fields must be exact")
canonical = prepare_correction(p["old_expected"], p["new_expected"],
comment_id=p["comment_id"], actor_id=p["actor_id"])
if canonical["head"] != intent.source_sha:
raise ValueError("correction head differs from intended source")
target = canonical["target"]
if intent.target != target["owner"] + "/" + target["repository"] or intent.target_id != canonical["comment_id"]:
raise ValueError("correction intent scope mismatch")
if not approval.correction:
raise ValueError("separate correction approval required")
return copy.deepcopy(intent)
def execute_rework_operation(adapter, *, intent, approval, declaration, coordinator, on_attempt):
"""Execute one validated publication operation through a coordinator.
The adapter must implement the shared trusted preflight/capability/mutate/
readback/verify contract and enforce exact provider scope at each boundary.
Adoption is rechecked immediately before the sole mutate callback.
"""
stable = _validate(intent, approval)
if stable.operation == "correction" and (adapter.capability != "ordinary_patch" or not approval.ordinary_patch):
raise ValueError("client-CAS correction is unsupported; explicit ordinary-PATCH exception required")
if not isinstance(coordinator, _ops.Coordinator):
raise ValueError("process-local coordinator required")
if not callable(on_attempt):
raise ValueError("attempt recorder required")
original_mutate = adapter.mutate
adoption_failed = {"value": False}
def guarded_mutate(candidate):
if candidate.operation == "push":
revision = candidate.payload["head"]
else:
revision = candidate.source_sha
try:
require_adoption(declaration, expected_source={"kind": "git_tree", "revision": revision},
expected_stage="implementation-delivery")
except Exception as exc:
adoption_failed["value"] = True
return {"acknowledgement": "failed", "error": type(exc).__name__}
return original_mutate(copy.deepcopy(candidate))
def guarded_verify(candidate, pre, observed):
if adoption_failed["value"]:
return False
return adapter.verify(copy.deepcopy(candidate), copy.deepcopy(pre), copy.deepcopy(observed))
wrapped = _ops.Adapter(preflight=adapter.preflight, capability=adapter.capability,
mutate=guarded_mutate, readback=adapter.readback, verify=guarded_verify)
return coordinator.execute(stable, approval, wrapped, on_attempt)
def verify_rework_reuse(adapter, *, intent, approval, declaration):
"""Fresh read-only reuse proof; supplied snapshots cannot authorize reuse."""
stable = _validate(intent, approval)
if stable.operation != "comment.create":
raise ValueError("reuse is only supported for canonical comment publication")
for name in ("preflight", "readback", "verify_reuse"):
if not callable(getattr(adapter, name, None)):
raise ValueError("trusted read-only reuse adapter required")
require_adoption(declaration, expected_source={"kind": "git_tree", "revision":stable.source_sha},
expected_stage="implementation-delivery")
snapshot = adapter.preflight(copy.deepcopy(stable))
if type(snapshot) is not dict or snapshot.get("fresh") is not True:
raise ValueError("fresh reuse snapshot required")
observed = adapter.readback(copy.deepcopy(stable))
if adapter.verify_reuse(copy.deepcopy(stable),copy.deepcopy(snapshot),copy.deepcopy(observed)) is not True:
raise ValueError("reuse verification incomplete")
return {"status":"verified", "action":"reuse", "external_write_performed":False}
__all__ = ["execute_rework_operation", "verify_rework_reuse", "OPERATIONS"]
+145
View File
@@ -0,0 +1,145 @@
"""In-memory coordinator for one approved impl mutation attempt.
Trusted adapter seam only; not a durable transaction or cross-process lock.
"""
from __future__ import annotations
import hashlib, json, re, copy, threading
from dataclasses import dataclass
from typing import Any, Callable
_HEX40 = re.compile(r"^[0-9a-f]{40}$")
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
_OPERATIONS = {"push", "pr.create", "pr.patch", "comment.create", "comment.patch", "correction"}
def _json(value: Any) -> bytes:
try: raw = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False).encode()
except (TypeError, ValueError, RecursionError) as exc: raise ValueError("data must be finite JSON-safe values") from exc
if len(raw) > 65536: raise ValueError("data exceeds 64 KiB")
return raw
def _digest(value: Any) -> str: return hashlib.sha256(_json(value)).hexdigest()
@dataclass(frozen=True)
class Intent:
version: str
operation: str
target: str
target_id: int
source_sha: str
payload: Any
@property
def digest(self) -> str: return _digest(self.as_dict())
def as_dict(self) -> dict[str, Any]:
return {"version":self.version,"operation":self.operation,"target":self.target,"target_id":self.target_id,"source_sha":self.source_sha,"payload":self.payload}
@dataclass(frozen=True)
class Approval:
version: str
intent_digest: str
status: str
correction: bool = False
ordinary_patch: bool = False
@dataclass
class Adapter:
preflight: Callable[[Intent], Any]
capability: Any
mutate: Callable[[Intent], Any]
readback: Callable[[Intent], Any]
verify: Callable[[Intent, Any, Any], Any]
@dataclass(frozen=True)
class Result:
status: str
intent_digest: str
snapshot_digest: str | None
attempt_digest: str | None
observed_digest: str | None
assurance: dict[str, Any]
reason: str
class UncertainAcknowledgement(Exception):
"""Write may have been accepted despite lost acknowledgement."""
def validate(intent: Intent, approval: Approval) -> Intent:
if not isinstance(intent, Intent) or type(intent.version) is not str or intent.version != "v1" or type(intent.operation) is not str or intent.operation not in _OPERATIONS: raise ValueError("unsupported intent schema or operation")
if not isinstance(intent.target,str) or not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+",intent.target) or any(part in (".", "..") for part in intent.target.split("/")): raise ValueError("invalid target")
if type(intent.target_id) is not int or intent.target_id <= 0: raise ValueError("invalid target id")
if type(intent.source_sha) is not str or not _HEX40.fullmatch(intent.source_sha): raise ValueError("source_sha must be full lowercase SHA-1")
_json(intent.payload)
if not isinstance(approval,Approval) or type(approval.version) is not str or approval.version!="v1" or type(approval.status) is not str or approval.status!="approved": raise ValueError("valid versioned approval required")
if not isinstance(approval.intent_digest,str) or not _HEX64.fullmatch(approval.intent_digest) or approval.intent_digest!=intent.digest: raise ValueError("approval is not bound to intent")
if type(approval.correction) is not bool or type(approval.ordinary_patch) is not bool: raise ValueError("approval flags must be booleans")
if intent.operation=="correction" and not approval.correction: raise ValueError("separate correction approval required")
return intent
class Coordinator:
def __init__(self):
self._consumed: set[str] = set()
self._reservation_lock = threading.Lock()
def _reserve(self, digest: str) -> bool:
with self._reservation_lock:
if digest in self._consumed:
return False
self._consumed.add(digest)
return True
def execute(self,intent:Intent,approval:Approval,adapter:Adapter,on_attempt:Callable[[dict[str,Any]],Any]|None=None)->Result:
assurance={"durable_at_most_once":False,"atomic_client_precondition":None,"residual_race":None,"trusted_adapter_contract":True}
try:
validate(intent,approval)
digest=intent.digest
authoritative=copy.deepcopy(intent.as_dict())
approved_digest=_digest(authoritative)
except Exception as exc:
return Result("partial","",None,None,None,assurance,f"invalid intent or approval: {type(exc).__name__}")
if on_attempt is not None and not callable(on_attempt): return Result("partial",digest,None,None,None,assurance,"attempt callback is not callable")
if digest in self._consumed: return Result("partial",digest,None,None,None,assurance,"attempt authority already consumed")
fields=("preflight","mutate","readback","verify")
if not isinstance(adapter,Adapter) or any(not callable(getattr(adapter,n,None)) for n in fields): return Result("partial",digest,None,None,None,assurance,"trusted adapter incomplete")
if intent.operation=="correction":
if adapter.capability is True: assurance["atomic_client_precondition"]=True
elif adapter.capability=="ordinary_patch" and approval.ordinary_patch:
assurance["atomic_client_precondition"]=False; assurance["residual_race"]="ordinary PATCH has read/write race; lease is not provider CAS"
else: return Result("partial",digest,None,None,None,assurance,"unsupported correction capability")
elif adapter.capability is not True: return Result("partial",digest,None,None,None,assurance,"unsupported capability")
# Reserve authority before invoking any trusted callback. A preflight can
# re-enter this coordinator; reserving only at mutate would allow two writes.
if not self._reserve(digest):
return Result("partial",digest,None,None,None,assurance,"attempt authority already consumed")
try:
pre=adapter.preflight(Intent(**copy.deepcopy(authoritative)))
pre_snapshot=copy.deepcopy(pre); snap=_digest(pre_snapshot)
if not isinstance(pre_snapshot,dict) or pre_snapshot.get("fresh") is not True: return Result("partial",digest,snap,None,None,assurance,"stale or invalid preflight")
except Exception as exc: return Result("partial",digest,None,None,None,assurance,f"preflight failed: {type(exc).__name__}")
# Callbacks receive disposable copies; the approved canonical form is authoritative.
attempt={"intent_digest":digest,"snapshot_digest":snap,"state":"attempted"}
self._consumed.add(digest)
if on_attempt is not None:
try: on_attempt(copy.deepcopy(attempt))
except Exception as exc: return Result("partial",digest,snap,_digest(attempt),None,assurance,f"attempt record failed: {type(exc).__name__}")
try:
validate(intent,approval)
if _digest(intent.as_dict()) != approved_digest: return Result("partial",digest,snap,None,None,assurance,"intent changed after approval")
except Exception as exc: return Result("partial",digest,snap,None,None,assurance,f"intent changed after approval: {type(exc).__name__}")
stable_intent=Intent(**copy.deepcopy(authoritative))
try: mutation=adapter.mutate(stable_intent)
except UncertainAcknowledgement: mutation={"acknowledgement":"uncertain"}
except Exception as exc: mutation={"acknowledgement":"failed","error":type(exc).__name__}
safe_mutation = mutation if _safe_ack(mutation) else {"acknowledgement":"unserializable-or-oversized"}
uncertain=isinstance(mutation,dict) and mutation.get("acknowledgement")=="uncertain"
try: observed=adapter.readback(Intent(**copy.deepcopy(authoritative))); observed_digest=_digest(observed)
except Exception as exc: return Result("partial",digest,snap,_digest({**attempt,"mutation":safe_mutation}),None,assurance,f"readback failed: {type(exc).__name__}")
try: verified=adapter.verify(Intent(**copy.deepcopy(authoritative)),copy.deepcopy(pre_snapshot),copy.deepcopy(observed))
except Exception as exc: return Result("partial",digest,snap,_digest({**attempt,"mutation":safe_mutation}),observed_digest,assurance,f"verification failed: {type(exc).__name__}")
if verified is not True: return Result("partial",digest,snap,_digest({**attempt,"mutation":safe_mutation}),observed_digest,assurance,"trusted adapter verification incomplete")
return Result("verified",digest,snap,_digest({**attempt,"mutation":safe_mutation,"uncertain_ack":uncertain}),observed_digest,assurance,"verified by trusted adapter readback")
def _safe_ack(value: Any) -> bool:
try: _json(value); return True
except Exception: return False
_DEFAULT_COORDINATOR=Coordinator()
def execute(intent:Intent,approval:Approval,adapter:Adapter,on_attempt:Callable[[dict[str,Any]],Any]|None=None)->Result:
"""Deprecated process-local compatibility wrapper; reuse is guarded within process."""
return _DEFAULT_COORDINATOR.execute(intent,approval,adapter,on_attempt)
@@ -0,0 +1,17 @@
"""Read only the explicitly required workflow reference for canonical impl skills."""
from pathlib import Path
NAMES = {"gitea-issue-implementation", "gitea-issue-rework"}
REQUIRED = "Required reference: [workflow](references/workflow.md)"
def read_contract(path):
path = Path(path)
text = path.read_text(encoding="utf-8")
if path.name == "SKILL.md" and path.parent.name in NAMES:
if REQUIRED not in text:
raise AssertionError("impl skill does not require its canonical workflow reference")
reference = path.parent / "references/workflow.md"
if reference.is_symlink() or not reference.is_file():
raise AssertionError("canonical workflow reference missing or symlinked")
text += "\n" + reference.read_text(encoding="utf-8")
return text
@@ -1,5 +1,6 @@
"""Executable consumer declaration contract wiring."""
from pathlib import Path
from harness.tests.common.impl_contract_docs import read_contract
import unittest
ROOT = Path(__file__).resolve().parents[3]
@@ -22,7 +23,7 @@ class DelegationAdoptionContractTests(unittest.TestCase):
'review/skills/gitea-code-review',
)
for path in paths:
text = (ROOT / 'harness/agents' / path / 'SKILL.md').read_text()
text = read_contract(ROOT / 'harness/agents' / path / 'SKILL.md')
with self.subTest(path=path):
self.assertIn('require_adoption', text)
self.assertIn('mode', text)
@@ -1,5 +1,6 @@
"""Procedural consumer wiring is not proof of live agent execution."""
from pathlib import Path
from harness.tests.common.impl_contract_docs import read_contract
import unittest
ROOT = Path(__file__).resolve().parents[2]
@@ -15,7 +16,7 @@ class DelegationEvidenceContractTests(unittest.TestCase):
def test_consumers_require_gate_before_adoption(self):
for path in ROLES:
with self.subTest(path=path):
text = (ROOT / path).read_text()
text = read_contract(ROOT / path)
for required in ['## Delegation evidence acceptance gate',
'delegation_evidence.py', '위임 결과 채택 이전',
'main이 독립적으로 고정한 context',
@@ -7,6 +7,7 @@ import subprocess
import tempfile
import unittest
from pathlib import Path
from harness.tests.common.impl_contract_docs import read_contract
ROOT = Path(__file__).resolve().parents[3]
POLICY = ROOT / "harness/operations/repository-cache.md"
@@ -38,7 +39,7 @@ class GitCredentialRecipeTests(unittest.TestCase):
self.assertIn(required, section)
policy = POLICY.read_text(encoding="utf-8")
self.assertIn("Lock 파일과 임시 credential helper는 실행 role 소유와 mode `0600`을 요구한다", policy)
delivery = REWORK.read_text(encoding="utf-8").split("### 7. 기존 PR branch 전달", 1)[1].split("###", 1)[0]
delivery = read_contract(REWORK).split("### 7. 기존 PR branch 전달", 1)[1].split("###", 1)[0]
self.assertIn("#non-file-git-credential-helper", delivery)
self.assertNotIn("필요한 temporary askpass helper", delivery)
@@ -1,6 +1,7 @@
"""Contract wiring for the read-only inventory source-binding preflight."""
import unittest
from pathlib import Path
from harness.tests.common.impl_contract_docs import read_contract
ROOT = Path(__file__).resolve().parents[2]
@@ -15,7 +16,7 @@ class InventoryBindingDocumentationTests(unittest.TestCase):
]
for path in paths:
with self.subTest(path=path):
text = (ROOT / path).read_text()
text = read_contract(ROOT / path)
self.assertIn('test_inventory_binding.py', text)
self.assertIn('check-tests 이전', text)
self.assertIn('독립적으로 고정한 revision과 stage', text)
@@ -7,6 +7,7 @@ import importlib.util
import json
import os
from pathlib import Path
from harness.tests.common.impl_contract_docs import read_contract
import subprocess
import sys
import tempfile
@@ -19,7 +20,7 @@ HEADING = '### Conditional pytest evidence recipe'
class PytestEvidenceRecipeTests(unittest.TestCase):
def recipe(self):
text = SKILL.read_text()
text = read_contract(SKILL)
self.assertIn(HEADING, text, 'conditional pytest recipe is missing')
section = text.split(HEADING, 1)[1].split('\n### ', 1)[0]
return section, json.loads(section.split('```json\n', 1)[1].split('\n```', 1)[0])
@@ -3,6 +3,7 @@ import subprocess
import sys
import unittest
from pathlib import Path
from harness.tests.common.impl_contract_docs import read_contract
ROOT = Path(__file__).resolve().parents[3]
SCRIPT = ROOT / "harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py"
@@ -29,7 +30,7 @@ class VerificationContractDocumentationTests(unittest.TestCase):
self.assertIn("성공 기록을 실패로 덮어쓰지 않는다", text)
for path in SKILLS.values():
with self.subTest(path=path.name):
role = path.read_text(encoding="utf-8")
role = read_contract(path)
gate = role.split("## Managed-lock broker gate", 1)[1].split("\n## ", 1)[0]
self.assertIn("broker-generated", gate)
self.assertIn("ready JSON", gate)
@@ -38,11 +39,11 @@ class VerificationContractDocumentationTests(unittest.TestCase):
def test_role_runtime_startup_precedes_and_does_not_replace_admission(self):
roles = [path for path in (ROOT / 'harness/agents').rglob('SKILL.md')
if 'broker-generated runtime' in path.read_text()]
if 'broker-generated runtime' in read_contract(path)]
self.assertEqual(len(roles), 6)
for path in roles:
with self.subTest(role=path.parent.name):
gate = path.read_text().split('## Managed-lock broker gate', 1)[1].split('\n## ', 1)[0]
gate = read_contract(path).split('## Managed-lock broker gate', 1)[1].split('\n## ', 1)[0]
self.assertIn('ready 이후 각 workflow의 진입 검증·승인 조건을 확인한다', gate)
self.assertIn('Runtime 생성은 approval이나 worktree admission을 대신하지 않는다', gate)
self.assertIn('ready JSON의 `handle`/`socket`', gate)
@@ -55,7 +56,7 @@ class VerificationContractDocumentationTests(unittest.TestCase):
self.assertIn("PYTHONDONTWRITEBYTECODE", plan["command_env_allowlist"])
def test_full_suite_tracked_background_reconciles_before_retry(self):
text = SKILLS['rework'].read_text()
text = read_contract(SKILLS['rework'])
heading = '### Tracked bounded background evidence execution'
self.assertIn(heading, text)
section = text.split(heading, 1)[1].split('\n### ', 1)[0]
@@ -78,7 +79,7 @@ class VerificationContractDocumentationTests(unittest.TestCase):
self.assertGreater(command["timeout_seconds"], 0)
self.assertLessEqual(command["timeout_seconds"], 3600)
for path in (CACHE, *SKILLS.values()):
text = path.read_text()
text = read_contract(path)
for token in ("evidence-manifest/v3", "timeout_seconds", "process group",
"validate-archived-v2", "TDD RED", "자동 재실행"):
with self.subTest(path=str(path), token=token):
@@ -92,7 +93,7 @@ class VerificationContractDocumentationTests(unittest.TestCase):
def test_all_roles_reference_canonical_external_artifacts(self):
for path in (CACHE, *SKILLS.values()):
with self.subTest(path=path.name):
text = path.read_text(encoding="utf-8")
text = read_contract(path)
self.assertIn(CANONICAL, text)
self.assertIn("evidence_gap", text)
self.assertIn("[REDACTED]", text)
@@ -100,7 +101,7 @@ class VerificationContractDocumentationTests(unittest.TestCase):
def test_examples_match_real_helper_cli_not_unsupported_options(self):
for path in (SKILLS["implementation"], SKILLS["rework"]):
section = path.read_text().split("## Canonical external verification evidence", 1)[1]
section = read_contract(path).split("## Canonical external verification evidence", 1)[1]
match = re.search(r"```sh\n(.*?)```", section, re.S)
assert match is not None
block = match.group(1).replace("\\\n", " ")
@@ -135,7 +136,7 @@ class VerificationContractDocumentationTests(unittest.TestCase):
def test_prepare_contract_binds_root_and_forbids_recursive_parent_creation(self):
for path in (CACHE, SKILLS["implementation"], SKILLS["rework"]):
with self.subTest(path=path.name):
text = path.read_text(encoding="utf-8")
text = read_contract(path)
self.assertIn("exact harness root", text)
self.assertIn("Path.mkdir(parents=True, mode=0o700)", text)
self.assertIn("자동 chmod", text)
@@ -163,7 +164,7 @@ class VerificationContractDocumentationTests(unittest.TestCase):
guard = text.split('### Shared workflow guard gates', 1)[1].split('Issue별 검증 실행 기록의 정본', 1)[0]
self.assertNotIn('or verification root', guard)
for path in (SKILLS['implementation'], SKILLS['rework']):
self.assertNotIn('outside product checkout and verification root', path.read_text())
self.assertNotIn('outside product checkout and verification root', read_contract(path))
self.assertIn('verification/<run-id>', guard)
def test_workflow_guard_readonly_scope_does_not_ban_approved_worker_edits(self):
@@ -173,13 +174,13 @@ class VerificationContractDocumentationTests(unittest.TestCase):
self.assertIn('쓰기 worker는 승인된 변경 allowlist', guard)
def test_workflow_guard_snapshot_protects_scope_before_delegation(self):
implementation = SKILLS["implementation"].read_text()
implementation = read_contract(SKILLS["implementation"])
self.assertIn("workflow_guard.py snapshot", implementation)
self.assertIn("compare-source --repo-root", implementation)
self.assertIn("immutable protected scope", implementation)
def test_workflow_guard_inventory_is_checked_before_adopting_results(self):
rework = SKILLS["rework"].read_text()
rework = read_contract(SKILLS["rework"])
self.assertIn("workflow_guard.py check-tests", rework)
self.assertIn("test inventory", rework)
self.assertIn("RED/GREEN records must be named", rework)
@@ -215,13 +216,13 @@ class VerificationContractDocumentationTests(unittest.TestCase):
masked = 'authorization: ' + '*' * 3 + ' verification'
helper = ROOT / 'harness/lib/workflow_guard.py'
for path in (helper, CACHE):
text = path.read_text()
text = read_contract(path)
with self.subTest(path=path.name):
self.assertEqual(text.count(declaration), 1)
self.assertNotIn(masked, text)
def test_rework_no_longer_excludes_repo_evidence_from_clean_tree(self):
text = SKILLS["rework"].read_text()
text = read_contract(SKILLS["rework"])
self.assertNotIn("plan과 manifest를 제외한 clean tree", text)
self.assertIn("새 evidence는 checkout 밖", text)
self.assertIn("archived v1 read-only", text)
@@ -0,0 +1,43 @@
"""Canonical impl contracts stay reachable without duplicated skill bodies."""
import re
import unittest
from pathlib import Path
from harness.tests.common.impl_contract_docs import read_contract, REQUIRED
ROOT = Path(__file__).resolve().parents[3]
class ImplArchitectureDocsTests(unittest.TestCase):
def test_skills_require_single_detailed_workflow_source(self):
for name in ("gitea-issue-implementation", "gitea-issue-rework"):
path = ROOT / "harness/agents/impl/skills" / name / "SKILL.md"
text = path.read_text()
reference = path.parent / "references/workflow.md"
with self.subTest(skill=name):
self.assertIn(REQUIRED, text)
self.assertLess(len(text.splitlines()), 100)
self.assertEqual(text.count("name: " + name), 1)
self.assertNotIn("## Managed-lock broker gate", text)
self.assertFalse(reference.read_text().startswith("---"))
contract = read_contract(path)
self.assertEqual(contract.count("## Managed-lock broker gate"), 1)
for gate in ("## Delegation evidence acceptance gate", "## Test inventory source-binding gate", "## Canonical external verification evidence"):
self.assertEqual(contract.count(gate), 1)
def test_all_canonical_impl_markdown_links_resolve(self):
for name in ("gitea-issue-implementation", "gitea-issue-rework"):
directory = ROOT / "harness/agents/impl/skills" / name
for path in [directory / "SKILL.md", directory / "references/workflow.md"]:
for link in re.findall(r"\[[^\]]+\]\(([^)]+)\)", path.read_text()):
if link.startswith(("https://", "http://", "#")):
continue
target = link.split("#", 1)[0]
with self.subTest(path=path.name, link=link):
self.assertTrue((path.parent / target).resolve().exists())
def test_correction_default_and_exception_remain_distinct(self):
text = (ROOT / "harness/agents/impl/skills/gitea-issue-rework/references/readback.md").read_text()
for token in ("strict", "separately", "atomic_client_precondition=false", "residual", "neutered"):
self.assertIn(token.lower(), text.lower())
if __name__ == "__main__":
unittest.main()
+152
View File
@@ -0,0 +1,152 @@
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT / "harness" / "lib"))
import impl_operation as op
class ImplOperationTests(unittest.TestCase):
def test_reentrant_preflight_cannot_authorize_duplicate_mutation(self):
coordinator = op.Coordinator()
intent = make_intent()
approval = make_approval(intent)
writes = []
entered = []
def preflight(value):
if not entered:
entered.append(True)
coordinator.execute(intent, approval, provider)
return {"fresh": True}
provider = adapter(preflight=preflight, mutate=lambda value: writes.append("write"))
coordinator.execute(intent, approval, provider)
self.assertEqual(writes, ["write"])
def test_preflight_cannot_change_approved_payload_before_mutation(self):
intent = make_intent()
calls = []
def preflight(i):
i.payload["body"] = "unapproved"
return {"fresh": True}
c = op.Coordinator()
result = c.execute(intent, make_approval(intent), adapter(preflight=preflight,
mutate=lambda i: calls.append(i.payload["body"])), calls.append)
self.assertEqual(calls[-1], "ok")
self.assertEqual(result.status, "verified")
def test_attempt_callback_must_be_callable_before_preflight(self):
calls = []
result = op.Coordinator().execute(make_intent(), make_approval(),
adapter(preflight=lambda i: calls.append("preflight") or {"fresh": True}), on_attempt=3)
self.assertEqual(calls, [])
self.assertEqual(result.status, "partial")
def test_attempt_callback_failure_consumes_authority_without_write(self):
calls = []
c = op.Coordinator()
a = adapter(mutate=lambda i: calls.append("write"))
result = c.execute(make_intent(), make_approval(), a,
lambda record: (_ for _ in ()).throw(RuntimeError("secret credential")))
self.assertEqual(calls, [])
self.assertNotIn("secret credential", result.reason)
self.assertEqual(c.execute(make_intent(), make_approval(), a).status, "partial")
def test_malformed_canonical_target_rejected(self):
for target in ("owner/..", "owner/.", "owner/repo\\x", "owner/repo\n"):
with self.subTest(target=target):
i = op.Intent("v1", "push", target, 1, "a"*40, {})
self.assertRaises(ValueError, op.validate, i, op.Approval("v1", "0"*64, "approved"))
def test_unserializable_ack_does_not_lose_verified_result(self):
result = op.Coordinator().execute(make_intent(), make_approval(), adapter(mutate=lambda i: object()))
self.assertEqual(result.status, "verified")
def test_verify_exception_returns_partial_and_no_retry(self):
calls = []
c = op.Coordinator()
a = adapter(mutate=lambda i: calls.append("write"), verify=lambda *args: (_ for _ in ()).throw(RuntimeError("private")))
result = c.execute(make_intent(), make_approval(), a)
self.assertEqual(result.status, "partial")
self.assertNotIn("private", result.reason)
c.execute(make_intent(), make_approval(), a)
self.assertEqual(calls, ["write"])
def test_versioned_intent_and_approval_validate(self):
intent = op.Intent("v1", "comment.patch", "owner/repo", 12, "a" * 40, {"body": "ok"})
approval = op.Approval("v1", intent.digest, "approved")
self.assertEqual(op.validate(intent, approval).digest, intent.digest)
def test_invalid_full_sha_rejected_before_callbacks(self):
calls = []
intent = op.Intent("v1", "push", "owner/repo", 1, "a" * 39, {})
result = op.Coordinator().execute(intent, op.Approval("v1", "0" * 64, "approved"), adapter(), calls.append)
self.assertEqual(result.status, "partial")
self.assertEqual(calls, [])
def test_stale_preflight_fails_without_mutation(self):
calls = []
result = op.Coordinator().execute(make_intent(), make_approval(), adapter(preflight=lambda i: {"fresh": False}), calls.append)
self.assertEqual(result.status, "partial")
self.assertEqual(calls, [])
def test_unsupported_capability_fails_before_write(self):
calls = []
result = op.Coordinator().execute(make_intent(), make_approval(), adapter(capability=False), calls.append)
self.assertEqual(result.status, "partial")
self.assertEqual(calls, [])
def test_uncertain_ack_reconciles_without_second_write(self):
calls = []
a = adapter(mutate=lambda i: (_ for _ in ()).throw(op.UncertainAcknowledgement()), readback=lambda i: {"state": "applied"}, verify=lambda i, p, r: True)
result = op.Coordinator().execute(make_intent(), make_approval(), a, calls.append)
self.assertEqual(len(calls), 1)
self.assertEqual(result.status, "verified")
self.assertEqual(result.assurance["durable_at_most_once"], False)
def test_reuse_cannot_mutate_twice(self):
calls = []
c = op.Coordinator()
a = adapter()
c.execute(make_intent(), make_approval(), a, calls.append)
again = c.execute(make_intent(), make_approval(), a, calls.append)
self.assertEqual(len(calls), 1)
self.assertEqual(again.status, "partial")
def test_verification_requires_trusted_adapter_callback(self):
calls = []
result = op.Coordinator().execute(make_intent(), make_approval(), adapter(verify=lambda *args: False), calls.append)
self.assertEqual(result.status, "partial")
self.assertNotEqual(result.status, "verified")
def test_correction_needs_separate_approval_unless_explicit_patch_exception(self):
calls = []
correction = make_intent(operation="correction")
result = op.Coordinator().execute(correction, make_approval(correction), adapter(capability="ordinary_patch"), calls.append)
self.assertEqual(result.status, "partial")
self.assertEqual(calls, [])
approval = make_approval(correction, correction=True, ordinary_patch=True)
result = op.Coordinator().execute(correction, approval, adapter(capability="ordinary_patch"), calls.append)
self.assertEqual(result.assurance["atomic_client_precondition"], False)
self.assertIn("residual_race", result.assurance)
def make_intent(operation="comment.patch"):
return op.Intent("v1", operation, "owner/repo", 12, "a" * 40, {"body": "ok"})
def make_approval(intent=None, correction=False, ordinary_patch=False):
intent = intent or make_intent()
return op.Approval("v1", intent.digest, "approved", correction=correction, ordinary_patch=ordinary_patch)
def adapter(**kwargs):
defaults = dict(preflight=lambda i: {"fresh": True}, capability=True,
mutate=lambda i: {"written": True}, readback=lambda i: {"state": "applied"},
verify=lambda i, p, r: True)
defaults.update(kwargs)
return op.Adapter(**defaults)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,117 @@
"""Integration regression tests for coordinator-backed initial delivery."""
import importlib.util
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
SCRIPT = ROOT / "harness/agents/impl/skills/gitea-issue-implementation/scripts/implementation_delivery.py"
sys.path.insert(0, str(ROOT))
spec = importlib.util.spec_from_file_location("implementation_delivery", SCRIPT)
delivery = importlib.util.module_from_spec(spec)
spec.loader.exec_module(delivery)
from harness.lib.impl_operation import Approval, Coordinator, Intent
HEAD = "a" * 40
class Adapter:
def __init__(self):
self.events=[]; self.number=7; self.fail_pr=False; self.uncertain=False; self.received=[]
def push_operation(self, intent):
self.received.append(intent)
return self.push(intent.payload['head'])
def create_or_reuse_pr_operation(self, intent):
self.received.append(intent)
return self.create_or_reuse_pr(intent.payload['head'])
def push(self, head): self.events.append("push"); return True
def read_remote_head(self): self.events.append("read-head"); return HEAD
def create_or_reuse_pr(self, head):
self.events.append("create-pr")
if self.fail_pr: raise RuntimeError("failed")
if self.uncertain: raise RuntimeError("uncertain ack")
return self.number
def read_pr(self, number): self.events.append("read-pr"); return {"number":number,"head":HEAD,"state":"open"}
def preflight_operation(self, intent): self.events.append("preflight:"+intent.operation); return {"fresh":True,"scope":intent.target}
def verify_operation(self, intent, before, observed): self.events.append("verify:"+intent.operation); return True
def find_intended_pr(self, head): self.events.append("find-pr"); return None if self.fail_pr else {"number":self.number}
def make_intents():
push=Intent("v1","push","org/repo",42,HEAD,{"head":HEAD})
pr=Intent("v1","pr.create","org/repo",42,HEAD,{"head":HEAD,"body":"approved body","base":"main"})
return push, Approval("v1",push.digest,"approved"), pr, Approval("v1",pr.digest,"approved")
class IntegrationTests(unittest.TestCase):
def invoke(self, adapter, intents=None, approvals=None, recorder=None):
p,pa,r,ra=make_intents(); intents=intents or (p,r); approvals=approvals or (pa,ra)
return delivery.deliver_approved_implementation(adapter,head=HEAD,declaration={"mode":"none"},push_intent=intents[0],push_approval=approvals[0],pr_intent=intents[1],pr_approval=approvals[1],coordinator=Coordinator(),on_attempt=recorder or (lambda entry: None))
def test_full_composed_delivery(self):
a=Adapter(); result=self.invoke(a)
self.assertEqual(result["status"],"verified"); self.assertEqual(result["artifact"]["pr"]["number"],7)
self.assertEqual(a.events.count("push"),1); self.assertEqual(a.events.count("create-pr"),1)
def test_uncertain_pr_acknowledgement_reconciles_without_second_write(self):
a = Adapter()
a.uncertain = True
result = self.invoke(a)
self.assertEqual(result["status"], "verified")
self.assertEqual(a.events.count("create-pr"), 1)
self.assertEqual(a.events.count("find-pr"), 1)
def test_adoption_rejection_never_promotes_existing_remote_state(self):
a = Adapter()
old = delivery._delivery_adoption.require_adoption
delivery._delivery_adoption.require_adoption = lambda *args, **kwargs: (_ for _ in ()).throw(ValueError("rejected"))
try:
result = self.invoke(a)
finally:
delivery._delivery_adoption.require_adoption = old
self.assertEqual(result["status"], "partial")
self.assertNotIn("push", a.events)
self.assertNotIn("create-pr", a.events)
def test_approved_payload_head_mismatch_rejected_before_any_write(self):
a = Adapter()
push, push_approval, pr, pr_approval = make_intents()
for operation in ('push', 'pr.create'):
candidate = Intent('v1', operation, 'org/repo', 42, HEAD, {'head':'f'*40,'body':'approved'})
candidate_approval = Approval('v1',candidate.digest,'approved')
intents = (candidate,pr) if operation == 'push' else (push,candidate)
approvals = (candidate_approval,pr_approval) if operation == 'push' else (push_approval,candidate_approval)
with self.subTest(operation=operation),self.assertRaises(ValueError):
self.invoke(a,intents=intents,approvals=approvals)
self.assertNotIn('push',a.events)
self.assertNotIn('create-pr',a.events)
def test_mutation_callbacks_receive_entire_approved_intent(self):
a = Adapter()
push, push_approval, pr, pr_approval = make_intents()
self.assertEqual(self.invoke(a)['status'],'verified')
self.assertEqual([item.as_dict() for item in a.received],[push.as_dict(),pr.as_dict()])
def test_new_adapter_contract_does_not_require_legacy_mutators(self):
a = Adapter()
a.push = None
a.create_or_reuse_pr = None
a.push_operation = lambda intent: a.events.append('push')
a.create_or_reuse_pr_operation = lambda intent: a.events.append('create-pr') or 7
self.assertEqual(self.invoke(a)['status'],'verified')
self.assertEqual(a.events.count('push'),1)
self.assertEqual(a.events.count('create-pr'),1)
def test_missing_approval_no_writes(self):
a=Adapter(); p,pa,r,ra=make_intents(); ra=Approval("v1","0"*64,"approved")
with self.assertRaises(ValueError): self.invoke(a,approvals=(pa,ra))
self.assertNotIn("push",a.events)
def test_preflight_failure_no_mutation(self):
a=Adapter(); a.preflight_operation=lambda intent:{"fresh":False}
result=self.invoke(a); self.assertEqual(result["status"],"partial"); self.assertNotIn("push",a.events)
def test_pr_failure_preserves_verified_push(self):
a=Adapter(); a.fail_pr=True; result=self.invoke(a)
self.assertEqual(result["status"],"partial"); self.assertEqual(result["push"].status,"verified"); self.assertIsNone(result["artifact"])
def test_mutations_each_require_fresh_adoption(self):
a=Adapter(); calls=[]; old=delivery._delivery_adoption.require_adoption
delivery._delivery_adoption.require_adoption=lambda *args,**kwargs:calls.append(kwargs["expected_stage"])
try: self.invoke(a)
finally: delivery._delivery_adoption.require_adoption=old
self.assertEqual(len(calls),4)
if __name__ == "__main__": unittest.main()
@@ -0,0 +1,135 @@
"""Functional correction verification with provider-shaped synthetic readbacks."""
import copy
import hashlib
import importlib.util
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from harness.tests.impl import test_rework_readback as fixtures
DIRECTORY = Path(__file__).parents[2] / 'agents/impl/skills/gitea-issue-rework/scripts'
spec = importlib.util.spec_from_file_location('correction_verifier', DIRECTORY/'rework_correction.py')
assert spec is not None and spec.loader is not None
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
class CorrectionVerifierTests(unittest.TestCase):
def setUp(self):
fixture = fixtures.ReworkReadbackTests()
fixture.setUp()
fixture.contract["version"] = "1.27.3"
fixture.overrides["/version"] = ({"version":"1.27.3"}, {}, 200)
self.addCleanup(fixture.doCleanups)
self.new = copy.deepcopy(fixture.expected)
self.old = copy.deepcopy(self.new)
free = self.new['marker_free_body']
for label in ('Source Issue', 'Pull Request', 'Source Review'):
free = free.replace('- '+label+': ', '- '+label+': #')
digest = hashlib.sha256(free.encode()).hexdigest()
marker = self.new['marker'].replace(self.new['result_sha256'], digest)
self.old.update(body=free+marker, marker=marker, marker_free_body=free, result_sha256=digest)
self.plan = fixture.legacy.prepare_correction(self.old,self.new,comment_id=501,actor_id=42)
fixture.add_result()
fixture.comments[0]['body'] = self.old['body']
self.before = fixture.adapter().snapshot(self.new)
fixture.comments[0]['body'] = self.new['body']
fixture.comments[0]['updated_at'] = '2026-01-03T00:00:00Z'
self.after = fixture.adapter().snapshot(self.new)
event = {'id':601,'type':'comment_ref','user':{'id':42,'login':'gitea-impl-agent'},
'created_at':fixtures.T1,'updated_at':fixtures.T1,'ref_action':'none',
'issue_url':fixtures.ORIGIN+'/acme/demo/issues/7',
'ref_issue':{'number':9,'html_url':fixtures.ORIGIN+'/acme/demo/pulls/9'},
'ref_comment':copy.deepcopy(self.before['raw']['comments']['rows'][0])}
self.before['source_issue_timeline'] = self.timeline(event)
changed = copy.deepcopy(event)
changed.update(ref_action='neutered',updated_at='2026-01-03T00:00:00Z',
ref_comment=copy.deepcopy(self.after['raw']['comments']['rows'][0]))
self.after['source_issue_timeline'] = self.timeline(changed)
self.approval = {'approved':True,'operation':'same_comment_correction',
'historical_failure_baseline_sha256':'1'*64,'old_expected_document':self.old,
'new_expected_document':self.new,'timeline_profile':'observed-comment-ref-neutralization',
'ordinary_patch':True,'provider_version':self.before['provider']['version']}
def timeline(self,event):
return {'complete':True,'rows':[event], 'pages':[{'page':1,'count':1,'headers':{'X-Total-Count':'1'}}]}
def verify(self,profile='observed-comment-ref-neutralization'):
return mod.verify_correction(self.plan,self.before,self.after,self.old['body'],self.new['body'],
self.approval,approval_sha256=mod.sha(mod.canonical(self.approval)),
historical_failure_sha256='1'*64,profile=profile)
def test_exact_approved_reference_neutralization_passes(self):
result=self.verify()
self.assertEqual(result['status'],'verified')
self.assertEqual(result['changed_timeline_ids'],[601])
self.assertEqual(result['historical_handoff_status'],'partial_unchanged')
def test_strict_unchanged_timeline_passes(self):
self.after['source_issue_timeline']=copy.deepcopy(self.before['source_issue_timeline'])
self.approval['timeline_profile']='strict'
self.assertEqual(self.verify('strict')['status'],'verified')
def test_reference_profile_requires_separate_approval(self):
self.approval['timeline_profile']='strict'
with self.assertRaises(mod.Bad): self.verify()
def test_source_and_cache_deltas_fail(self):
for name,field in [('source_issue','title'),('pr','comments'),('pr_issue','comments')]:
original=copy.deepcopy(self.after)
self.after['raw'][name][field] = 99 if field=='comments' else 'changed'
with self.subTest(name=name),self.assertRaises(mod.Bad): self.verify()
self.after=original
def test_unrelated_timeline_field_change_fails(self):
self.after['source_issue_timeline']['rows'][0]['body']='unexpected'
with self.assertRaises(mod.Bad): self.verify()
def test_unrelated_null_timeline_field_is_still_a_mutation(self):
self.after['source_issue_timeline']['rows'][0]['unexpected_field'] = None
with self.assertRaises(mod.Bad): self.verify()
def test_duplicate_and_missing_timeline_rows_fail(self):
original=copy.deepcopy(self.after)
for rows in [[],self.after['source_issue_timeline']['rows']*2]:
self.after=copy.deepcopy(original)
self.after['source_issue_timeline']['rows']=rows
with self.subTest(rows=len(rows)),self.assertRaises(mod.Bad): self.verify()
def test_wrong_actor_head_and_extra_body_change_fail(self):
for field,value in [('actor_id',99),('head','d'*40),('new_body','arbitrary')]:
original=copy.deepcopy(self.plan)
self.plan[field]=value
with self.subTest(field=field),self.assertRaises(mod.Bad): self.verify()
self.plan=original
def test_reference_timestamp_cannot_move_backwards(self):
self.after['source_issue_timeline']['rows'][0]['updated_at']='2025-01-01T00:00:00Z'
with self.assertRaises(mod.Bad): self.verify()
def test_real_cli_valid_inputs_and_no_overwrite(self):
with tempfile.TemporaryDirectory() as td:
root=Path(td)
args=[sys.executable,'-B',str(DIRECTORY/'rework_correction.py'),'verify']
values={'plan':self.plan,'before':self.before,'after':self.after,
'old-comment':{'body':self.old['body']},'new-comment':{'body':self.new['body']},'approval':self.approval}
for name,value in values.items():
raw=json.dumps(value).encode(); path=root/(name+'.json'); path.write_bytes(raw); path.chmod(0o600)
args.extend(['--'+name,str(path),'--'+name+'-sha256',hashlib.sha256(raw).hexdigest()])
output=root/'receipt.json'
args.extend(['--historical-failure-sha256','1'*64,'--profile','observed-comment-ref-neutralization','--output',str(output)])
result=subprocess.run(args,capture_output=True,text=True,timeout=10)
self.assertEqual(result.returncode,0,result.stderr)
saved=output.read_bytes()
again=subprocess.run(args,capture_output=True,text=True,timeout=10)
self.assertNotEqual(again.returncode,0)
self.assertEqual(output.read_bytes(),saved)
def test_digest_tampering_is_rejected(self):
with tempfile.TemporaryDirectory() as td:
path=Path(td)/'data.json';path.write_text('{}')
with self.assertRaises(mod.Bad):mod.load(path,'0'*64)
if __name__=='__main__':unittest.main()
@@ -0,0 +1,163 @@
"""Regression tests for main-owned rework publication integration."""
import copy
import hashlib
import sys
import unittest
from pathlib import Path
from unittest.mock import Mock
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "agents/impl/skills/gitea-issue-rework/scripts"
sys.path.insert(0, str(SCRIPT))
sys.path.insert(0, str(ROOT))
import rework_publication as pub
from harness.lib import impl_operation as ops
HEAD = "a" * 40
def expected():
# Generate through the canonical renderer to avoid hand-authored bodies.
import rework_comment as rc
d = dict(owner="o", repository="r", issue=4, pr=5, review=6,
reviewed_head="b"*40, final_head=HEAD, contract_sha256="c"*64,
result="partial", findings=[dict(id="F1", status="resolved", classification="valid_fix",
changes="changed safely", verification="checks pass", reason="verified")],
local_checks="pass", runtime_browser="not run", commit_status="없음", actions_runs="없음")
dd, vals, fs, extras, cs, ar = rc.canon(d)
body = rc.render_body(dd, vals, fs, extras, cs, ar)
rh = hashlib.sha256(body.encode()).hexdigest()
corr = hashlib.sha256("\0".join(["o", "r", "4", "5", "6", "b"*40, HEAD, "c"*64]).encode()).hexdigest()
marker = f"<!-- gitea-agent:implementation-rework/v1 owner=o repository=r issue=4 pr=5 review=6 reviewed-head={'b'*40} head={HEAD} contract-sha256={'c'*64} result-sha256={rh} correlation={corr} -->\n"
return dict(body=body+marker, marker=marker, marker_free_body=body, result_sha256=rh, correlation=corr,
target=dict(owner="o", repository="r", issue=4, pr=5), review=6,
reviewed_head="b"*40, head=HEAD, contract_sha256="c"*64)
def setup_case(op="comment.create", payload=None, approval_flags=None):
if payload is None:
e=expected(); payload={"expected":e,"target":e["target"]}
intent=ops.Intent("v1", op, "o/r", 5, HEAD, payload)
approval=ops.Approval("v1", intent.digest, "approved", **(approval_flags or {}))
declaration={"mode":"none"}
calls=[]
adapter=ops.Adapter(preflight=lambda i: {"fresh":True,"scope":"exact"}, capability=True,
mutate=lambda i: calls.append(copy.deepcopy(i)) or {"acknowledgement":"ok"},
readback=lambda i: {"scope":"exact","head":HEAD}, verify=lambda i,p,o: True)
return intent,approval,declaration,calls,adapter
class PublicationTests(unittest.TestCase):
def test_entrypoint_is_present_not_import_error(self):
self.assertTrue(callable(getattr(pub,"execute_rework_operation",None)))
def test_invalid_body_target_head_approval_do_not_write(self):
cases=[]
cases.append(setup_case("pr.patch", {"body":"body", "closing":"#4"}))
cases.append(setup_case("comment.create", {"expected":expected(),"target":{"owner":"evil","repository":"r","issue":4,"pr":5}}))
p={"head":"f"*40}; cases.append(setup_case("push",p))
for intent,approval,decl,calls,adapter in cases:
with self.subTest(intent=intent.operation), self.assertRaises(Exception):
pub.execute_rework_operation(adapter,intent=intent,approval=approval,declaration=decl,
coordinator=ops.Coordinator(),on_attempt=lambda x:None)
self.assertEqual(calls,[])
def test_single_mutation_and_fresh_adoption_at_boundary(self):
intent,approval,decl,calls,adapter=setup_case()
old=pub.require_adoption
try:
adopted=Mock(return_value=None); pub.require_adoption=adopted
attempts=[]
result=pub.execute_rework_operation(adapter,intent=intent,approval=approval,declaration=decl,
coordinator=ops.Coordinator(),on_attempt=attempts.append)
self.assertEqual(result.status,"verified")
self.assertEqual(len(calls),1); self.assertEqual(len(attempts),1)
adopted.assert_called_once()
finally: pub.require_adoption=old
def test_adoption_and_preflight_failure_have_zero_mutations(self):
intent,approval,decl,calls,adapter=setup_case()
old=pub.require_adoption
try:
pub.require_adoption=Mock(side_effect=ValueError("stale"))
r=pub.execute_rework_operation(adapter,intent=intent,approval=approval,declaration=decl,
coordinator=ops.Coordinator(),on_attempt=lambda x:None)
self.assertEqual(r.status,"partial"); self.assertEqual(calls,[])
adapter.preflight=lambda i: {"fresh":False}
r=pub.execute_rework_operation(adapter,intent=intent,approval=approval,declaration=decl,
coordinator=ops.Coordinator(),on_attempt=lambda x:None)
self.assertEqual(r.status,"partial"); self.assertEqual(calls,[])
finally: pub.require_adoption=old
def test_uncertain_ack_readback_once_literal_true_required(self):
intent,approval,decl,calls,adapter=setup_case()
old=pub.require_adoption
try:
pub.require_adoption=Mock(return_value=None)
mutations=[]; reads=[]
adapter.mutate=lambda i: mutations.append(i) or (_ for _ in ()).throw(ops.UncertainAcknowledgement())
adapter.readback=lambda i: reads.append(i) or {"head":HEAD}
adapter.verify=lambda i,p,o: 1
result=pub.execute_rework_operation(adapter,intent=intent,approval=approval,declaration=decl,
coordinator=ops.Coordinator(),on_attempt=lambda x:None)
self.assertEqual(result.status,"partial")
self.assertEqual((len(mutations),len(reads)),(1,1))
finally: pub.require_adoption=old
def test_reuse_collects_fresh_state_without_any_mutation(self):
intent, approval, declaration, calls, adapter = setup_case()
reads = []
adapter.preflight = lambda value: reads.append("preflight") or {"fresh":True}
adapter.readback = lambda value: reads.append("readback") or {"exact":True}
adapter.verify_reuse = lambda value, before, after: after.get("exact") is True
result = pub.verify_rework_reuse(adapter,intent=intent,approval=approval,declaration=declaration)
self.assertEqual(result['action'],'reuse')
self.assertEqual(reads,['preflight','readback'])
self.assertEqual(calls,[])
def test_reuse_stale_preflight_blocks_even_when_callback_would_accept(self):
intent, approval, declaration, calls, adapter = setup_case()
reads=[]
adapter.preflight=lambda value:{"fresh":False}
adapter.readback=lambda value:reads.append(True)
adapter.verify_reuse=lambda *args:True
with self.assertRaises(ValueError):
pub.verify_rework_reuse(adapter,intent=intent,approval=approval,declaration=declaration)
self.assertEqual(reads,[])
self.assertEqual(calls,[])
def test_canonical_comment_cannot_publish_to_different_intent_scope(self):
intent, approval, declaration, calls, adapter = setup_case()
for target, target_id in [('evil/r', 5), ('o/r', 999)]:
changed = ops.Intent('v1', intent.operation, target, target_id, HEAD, intent.payload)
approved = ops.Approval('v1', changed.digest, 'approved')
with self.subTest(target=target, target_id=target_id), self.assertRaises(ValueError):
pub.execute_rework_operation(adapter,intent=changed,approval=approved,declaration=declaration,
coordinator=ops.Coordinator(),on_attempt=lambda record:None)
self.assertEqual(calls, [])
def test_unimplemented_client_cas_correction_blocks_before_write(self):
import rework_comment as rc
new = expected()
old = copy.deepcopy(new)
body = new['marker_free_body']
for label in ('Source Issue','Pull Request','Source Review'):
body = body.replace('- '+label+': ','- '+label+': #')
digest = hashlib.sha256(body.encode()).hexdigest()
marker = new['marker'].replace(new['result_sha256'],digest)
old.update(body=body+marker,marker=marker,marker_free_body=body,result_sha256=digest)
payload = {'old_expected':old,'new_expected':new,'comment_id':7,'actor_id':42}
intent = ops.Intent('v1','correction','o/r',7,HEAD,payload)
approval = ops.Approval('v1',intent.digest,'approved',correction=True)
calls = []
adapter = ops.Adapter(lambda value:{'fresh':True},True,
lambda value:calls.append('write'),lambda value:{'exact':True},lambda *args:True)
with self.assertRaises(ValueError):
pub.execute_rework_operation(adapter,intent=intent,approval=approval,declaration={'mode':'none'},
coordinator=ops.Coordinator(),on_attempt=lambda record:None)
self.assertEqual(calls,[])
def test_generic_comment_patch_rejected_and_correction_requires_explicit_approval(self):
intent,approval,decl,calls,adapter=setup_case("comment.patch",{"body":"arbitrary"})
with self.assertRaises(ValueError): pub.execute_rework_operation(adapter,intent=intent,approval=approval,declaration=decl,coordinator=ops.Coordinator(),on_attempt=lambda x:None)
self.assertEqual(calls,[])
if __name__ == "__main__": unittest.main()
+2 -1
View File
@@ -7,6 +7,7 @@ import unittest
from argparse import Namespace
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
from harness.tests.common.impl_contract_docs import read_contract
DIRECTORY = Path(__file__).parents[2] / 'agents/impl/skills/gitea-issue-rework/scripts'
ORIGIN = 'https://gitea.jsfamily.quest'
@@ -696,7 +697,7 @@ class ReworkReadbackTests(unittest.TestCase):
def test_shared_commit_message_safety_rule_is_linked_and_amend_is_narrow(self):
common = (DIRECTORY.parents[4]/'operations/repository-cache.md').read_text()
skill = (DIRECTORY.parent/'SKILL.md').read_text()
skill = read_contract(DIRECTORY.parent / 'SKILL.md')
self.assertIn('## Commit-message reference safety', common)
for clause in ('complete subject and body', 'cross-repository',
'non-default branch', 'explicit user authorization',