From 8deeec683959aabfaa354356b8f1bcd1e8c1c648 Mon Sep 17 00:00:00 2001 From: cskwork Date: Thu, 17 Sep 2026 21:50:43 +0900 Subject: [PATCH 1/2] feat: three-lens verification, bound origin, machine-readable fix loop (v0.11.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build verification runs as three parallel fresh-context lenses under one role (roles/verifier.md): E2E, side effects with data consistency, and intent match against the request's origin. origin.md snapshots the ticket / 기획서 and is bound by the intent gate and every downstream gate. Success criteria are O-numbered and cited by spec requirements; plan.md names the data touched. The fix-loop cap is machine-readable (fixloop.exhausted → needs-human at every lazymode) and .sdlc/verify.md gains a receipted `data` check kind. Validation: selftest PASS; e2e 146/0; autotest 195/0; bash -n clean. --- AGENTS.md | 31 +++++--- README.ko.md | 7 +- README.md | 7 +- VERSION | 2 +- docs/automation.md | 6 ++ gates/_auto.sh | 38 +++++++-- gates/_common.sh | 14 +++- gates/approve.sh | 2 +- gates/autotest.sh | 37 +++++++++ gates/check-gate.sh | 6 +- gates/close.sh | 6 +- gates/e2e.sh | 18 +++++ gates/selftest.sh | 20 +++++ gates/status.sh | 16 +++- init.sh | 4 +- log/changelog-v0.11.0.md | 34 ++++++++ roles/adversary.md | 9 ++- roles/verifier.md | 165 +++++++++++++++++++++------------------ skills/1-intent/SKILL.md | 8 ++ skills/2-spec/SKILL.md | 5 +- skills/3-plan/SKILL.md | 4 + skills/4-build/SKILL.md | 84 ++++++++++---------- skills/5-ship/SKILL.md | 35 +++------ templates/deviations.md | 4 +- templates/evidence.md | 26 +++--- templates/intent.md | 10 ++- templates/origin.md | 16 ++++ templates/plan.md | 6 ++ templates/spec.md | 5 +- templates/verify.md | 5 +- tools/auto.sh | 19 ++++- 31 files changed, 439 insertions(+), 210 deletions(-) create mode 100644 log/changelog-v0.11.0.md create mode 100644 templates/origin.md diff --git a/AGENTS.md b/AGENTS.md index df16bf0..fe8f8db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,7 +74,9 @@ when closing. reviewed>"`, not a human ask. **What a gate binds**: the approval record names the canonical `.sdlc/work//` path, the artifact's sha256, and the - digests of the upstream artifacts it was granted on top of. Editing the + digests of the upstream artifacts it was granted on top of — `origin.md`, + the snapshot of the ticket or 기획서 the request came from, included + whenever it exists (templates/origin.md). Editing the approved artifact, or materially editing an upstream one, closes the gate with the exact re-approval command — a downstream gate never outlives the text it was granted for. The digest is CHANGE DETECTION, not @@ -254,18 +256,25 @@ when closing. 6. **Proof over claims.** Every "done" claim carries command output, using the real commands in `.sdlc/config.md`. - **Verification runs the real thing.** Before a feature ships, the changed - behavior is exercised end to end through the interface a user or caller - actually meets — the real screen for a UI change, a real request or command - against a running instance for an API/CLI/job change, and for a bug fix the - SAME failing flow before and after plus the neighbouring flows that share - the changed code. It is scoped to the change, reuses the project's own - commands (`.sdlc/config.md`: `e2e:`, `qa:`, `run:`), and never means - re-running the whole product suite as a ritual. Record command or tool, + **Verification runs the real thing, through three lenses.** Before a + feature ships, a fresh-context verifier (roles/verifier.md) checks it three + ways, in parallel. **E2E**: the changed behavior exercised through the + interface a user or caller actually meets, with the project's own commands + (`.sdlc/config.md`: `e2e:`, `qa:`, `run:`), scoped to the change — never + the whole product suite as a ritual. **Side effects**: what else changed + between AS-IS and TO-BE — the baseline, the untouched items, and the + consistency of every data shape the change writes or reads across its other + producers and consumers. **Intent match**: the build read back against the + origin of the request — `origin.md`, the snapshot the intent gate bound, + plus the live ticket or 기획서 when reachable — per intent.md O-item, + naming what is covered, missing, and beyond. Each check records command or tool, environment, scenario, and the observed result. **No environment to run it in = NOT VERIFIED**: say what is missing, in evidence.md. A passing unit suite is never a silent substitute, and a delivery over a known gap is - allowed only when the human accepts that gap explicitly. + allowed only when the human accepts that gap explicitly. A finding from any + lens enters the build fix loop (skills/4-build): three rounds, then the + human — `tools/auto.sh` reads the round lines in deviations.md and reports + an exhausted loop as `fixloop.exhausted`, needs-human at every lazymode. **A receipt makes a missing proof detectable** (optional, and the loop works without it). A project that fills `.sdlc/verify.md` @@ -333,7 +342,7 @@ when closing. not call it a confirmed fix. 7. **Artifacts live in the project repo** under `.sdlc/work//` while open and `.sdlc/archive//` after close. Git keeps the - durable record — `intent.md`, `spec.md`, `plan.md`, `map.md`, + durable record — `origin.md`, `intent.md`, `spec.md`, `plan.md`, `map.md`, `evidence.md`, `delivery.md`, `CLOSED`, `memory/`, `config.md`: the decisions and the final proof, readable a year later without the working copy. Gitignored working residue stays local (`approvals/`, diff --git a/README.ko.md b/README.ko.md index e73439a..7ac313f 100644 --- a/README.ko.md +++ b/README.ko.md @@ -26,7 +26,7 @@ Intent → spec → plan → build → evidence → maintain. 사람 승인 게 - `intent.md`의 모든 주장에 `[verified]` 또는 `[assumed]` 라벨이 붙습니다. - 새 컨텍스트의 adversary가 스펙을 먼저 공격한 뒤에 사람이 승인합니다. - 평범한 계획은 adversary 리뷰만 통과하면 자동 승인됩니다. 마이그레이션, 삭제, API, 보안, 인프라 변경은 사람에게 올라옵니다. -- 구현은 작성자가 아닌 별도의 verifier가 승인된 산출물과 대조합니다. +- 구현은 작성자가 아닌 별도의 verifier가 세 갈래로 병렬 검증합니다. 실제 동작(E2E), 부작용과 데이터 정합성, 그리고 요청의 출처인 티켓·기획서와의 일치 여부입니다. - 실패한 시도는 교훈과 도메인 지식으로 남아 다음 실행을 돕습니다. [Anthropic의 AI-Native SDLC 플레이북](https://claude.com/blog/the-ai-native-sdlc-playbook)을 옮긴 것이지만 Claude Code에 묶여 있지 않습니다. 구현체는 순수 Markdown과 셸 스크립트입니다. 파일을 읽고 명령을 실행할 수 있는 하네스라면 어디서든 돌아갑니다. @@ -146,6 +146,7 @@ agent APPROVED: intent of claims-status (.sdlc/work/claims-status/intent.md) │ ├── DOMAIN.md # 용어 · 확인된 사실 · 제약 │ └── lessons/-.md ├── work// # 열린 피처만 +│ ├── origin.md # 요청 당시의 티켓 · 기획서 스냅샷 — intent 게이트가 결합 │ ├── intent.md # 문제 · 증명 · 성공 기준 · 범위 │ ├── spec.md # Human summary · AS-IS → TO-BE · 계약 │ ├── plan.md # 파일 · 순서 · 리스크 · 증명 @@ -161,7 +162,7 @@ agent APPROVED: intent of claims-status (.sdlc/work/claims-status/intent.md) └── approvals/ # 피처의 승인 기록도 함께 이동, 여전히 gitignore 대상 ``` -`init.sh`는 프로젝트 `.gitignore`에 열두 줄을 추가합니다. `work/`와 `archive/` 양쪽의 `approvals/`, `baseline.txt`, `deviations.md`, `harvest.md`, `scratch/`, `progress.md`입니다. git에 남는 것은 지속 기록입니다. `config.md`, `memory/`, 그리고 피처마다 `intent.md`, `spec.md`, `plan.md`, `map.md`, `evidence.md`, `delivery.md`, 아카이브의 `CLOSED`. 결정과 최종 증거는 작업 사본 없이도 1년 뒤에 읽을 수 있어야 하기 때문입니다. 대용량 출력은 `scratch/`에 남고 evidence.md는 결정적인 줄만 인용합니다. 예전 킷으로 심은 프로젝트에서 `init.sh`를 다시 돌리면 그때 추가했던 `spec.md`·`evidence.md` 무시 줄을 제거하며, git 인덱스는 건드리지 않습니다. 피처가 열려 있는 동안 `status.sh`가 하트비트를 나이와 함께 `now →` 줄로 보여주며, `watch -n5 cat .sdlc/work//progress.md`로 실시간 추적할 수 있습니다. +`init.sh`는 프로젝트 `.gitignore`에 열두 줄을 추가합니다. `work/`와 `archive/` 양쪽의 `approvals/`, `baseline.txt`, `deviations.md`, `harvest.md`, `scratch/`, `progress.md`입니다. git에 남는 것은 지속 기록입니다. `config.md`, `memory/`, 그리고 피처마다 `origin.md`, `intent.md`, `spec.md`, `plan.md`, `map.md`, `evidence.md`, `delivery.md`, 아카이브의 `CLOSED`. 결정과 최종 증거는 작업 사본 없이도 1년 뒤에 읽을 수 있어야 하기 때문입니다. 대용량 출력은 `scratch/`에 남고 evidence.md는 결정적인 줄만 인용합니다. 예전 킷으로 심은 프로젝트에서 `init.sh`를 다시 돌리면 그때 추가했던 `spec.md`·`evidence.md` 무시 줄을 제거하며, git 인덱스는 건드리지 않습니다. 피처가 열려 있는 동안 `status.sh`가 하트비트를 나이와 함께 `now →` 줄로 보여주며, `watch -n5 cat .sdlc/work//progress.md`로 실시간 추적할 수 있습니다. 공개 sdlc-kit 저장소는 프레임워크만 담습니다. 커밋되는 산출물(intent, plan, map, memory)은 그것이 설명하는 프로젝트 안에서 함께 버전 관리됩니다. 무시되는 나머지는 그것을 만든 작업 사본 안에만 남습니다. @@ -193,7 +194,7 @@ ship 승인이 묶는 것은 리뷰가 본 프로젝트 소스 전체 스냅샷 실행 권한은 Git의 `core.filemode` 설정에 따라 판단합니다. Windows Git Bash처럼 값이 `false`이면 추적 중인 파일은 Git 인덱스의 실행 권한을 사용하고 새 파일은 실행 권한이 없는 것으로 처리합니다. 실행 파일로 지정하려면 리뷰 전에 `git add --chmod=+x` 또는 `git update-index --chmod=+x`를 사용하세요. 리뷰 후 인덱스의 실행 권한을 바꾸면 승인이 무효화됩니다. `core.filemode=true`인 환경에서는 파일 시스템의 chmod 변경을 직접 검사합니다. -그 전에 검증은 실제 동작을 돌립니다. 바뀐 동작을 사용자나 호출자가 실제로 만나는 인터페이스로 끝까지 실행하되, 변경 범위에 맞춰 프로젝트 자신의 명령(`.sdlc/config.md`의 `e2e:`, `qa:`, `run:`)을 씁니다. 실행할 환경이 없으면 NOT VERIFIED이며 evidence.md에 그렇게 적습니다. 통과한 단위 테스트가 조용한 대체물이 되는 일은 없습니다. +그 전에 검증은 실제 동작을 돌립니다. 바뀐 동작을 사용자나 호출자가 실제로 만나는 인터페이스로 끝까지 실행하되, 변경 범위에 맞춰 프로젝트 자신의 명령(`.sdlc/config.md`의 `e2e:`, `qa:`, `run:`)을 씁니다. 실행할 환경이 없으면 NOT VERIFIED이며 evidence.md에 그렇게 적습니다. 통과한 단위 테스트가 조용한 대체물이 되는 일은 없습니다. 그 옆에서 두 갈래가 병렬로 더 돕니다. **부작용** 렌즈는 베이스라인, 유지되어야 할 동작, 그리고 변경이 건드린 데이터 형태가 다른 생산자와 소비자 사이에서 정합성을 지키는지 봅니다. **의도 일치** 렌즈는 intent 게이트가 결합한 티켓·기획서 스냅샷 `origin.md`를 번호 붙은 성공 기준마다 대조해, 구현이 무엇을 담았고 무엇을 빠뜨렸고 무엇을 넘어섰는지 적습니다. 어느 렌즈의 발견이든 build의 fix loop로 들어가며, 3라운드 안에 해결되지 않으면 사람에게 가고 `tools/auto.sh`는 이를 `fixloop.exhausted`로 보고합니다. ### 실패한 실행도 지식을 남긴다 diff --git a/README.md b/README.md index ae596de..61965a2 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ A common agent workflow starts with implementation. The agent receives a prompt, - Claims in `intent.md` are labeled `[verified]` or `[assumed]`. - A fresh-context adversary reviews the spec before you approve it. - A routine plan auto-approves after a clean adversary review; migrations, deletions, API, security, and infra changes escalate to you. -- A different verifier checks the implementation against the approved artifacts. +- A different verifier checks the implementation three ways in parallel: end to end, for side effects and data consistency, and against the ticket or spec document the request came from. - Failed attempts leave lessons and domain knowledge for the next run. It is adapted from [Anthropic's AI-Native SDLC playbook](https://claude.com/blog/the-ai-native-sdlc-playbook), but it does not depend on Claude Code. The implementation is plain Markdown plus shell scripts. Any harness that can read files and run commands can use it. @@ -146,6 +146,7 @@ Per feature, inside the **target project**: │ ├── DOMAIN.md # terms · verified facts · constraints │ └── lessons/-.md ├── work// # OPEN features only +│ ├── origin.md # the ticket / 기획서 as requested — bound by the intent gate │ ├── intent.md # problem · proof · success · scope │ ├── spec.md # Human summary · AS-IS → TO-BE · contract │ ├── plan.md # files · order · risks · proof @@ -161,7 +162,7 @@ Per feature, inside the **target project**: └── approvals/ # moves with the feature, still gitignored ``` -`init.sh` also adds twelve lines to the project's `.gitignore`, covering `work/` and `archive/` alike: `approvals/`, `baseline.txt`, `deviations.md`, `harvest.md`, `scratch/`, and `progress.md`. Git keeps the durable record — `config.md`, `memory/`, and per feature `intent.md`, `spec.md`, `plan.md`, `map.md`, `evidence.md`, `delivery.md`, and the archived `CLOSED` — so the decisions and the final proof survive without the working copy. Bulk output stays in `scratch/`, cited by the deciding lines quoted in evidence.md. Re-running `init.sh` on a project seeded by an older kit removes the ignore lines it once issued for `spec.md` and `evidence.md`; it never touches the git index. While a feature is open, `status.sh` shows the heartbeat as a `now →` line with its age — `watch -n5 cat .sdlc/work//progress.md` follows it live. +`init.sh` also adds twelve lines to the project's `.gitignore`, covering `work/` and `archive/` alike: `approvals/`, `baseline.txt`, `deviations.md`, `harvest.md`, `scratch/`, and `progress.md`. Git keeps the durable record — `config.md`, `memory/`, and per feature `origin.md`, `intent.md`, `spec.md`, `plan.md`, `map.md`, `evidence.md`, `delivery.md`, and the archived `CLOSED` — so the decisions and the final proof survive without the working copy. Bulk output stays in `scratch/`, cited by the deciding lines quoted in evidence.md. Re-running `init.sh` on a project seeded by an older kit removes the ignore lines it once issued for `spec.md` and `evidence.md`; it never touches the git index. While a feature is open, `status.sh` shows the heartbeat as a `now →` line with its age — `watch -n5 cat .sdlc/work//progress.md` follows it live. The public sdlc-kit repository stays framework-only. The committed artifacts — intent, plan, map, memory — live and version with the project they describe. The ignored ones live only in the working copy that produced them. @@ -193,7 +194,7 @@ The ship approval binds the project's whole source snapshot as the review saw it Executable bits follow Git's `core.filemode` setting. When it is `false`, as on Git Bash for Windows, the snapshot uses the index mode for tracked files and treats new files as non-executable. Use `git add --chmod=+x` or `git update-index --chmod=+x` before review to mark an executable; changing that index mode after review invalidates approval. With `core.filemode=true`, filesystem chmod changes are checked directly. -Before any of that, verification runs the real thing: the changed behavior exercised end to end through the interface a user or caller actually meets, scoped to the change, with the project's own commands (`e2e:`, `qa:`, `run:` in `.sdlc/config.md`). No environment to run it in means NOT VERIFIED, stated as such in evidence.md — a green unit suite is never a silent substitute. +Before any of that, verification runs the real thing: the changed behavior exercised end to end through the interface a user or caller actually meets, scoped to the change, with the project's own commands (`e2e:`, `qa:`, `run:` in `.sdlc/config.md`). No environment to run it in means NOT VERIFIED, stated as such in evidence.md — a green unit suite is never a silent substitute. Two more lenses run beside it in parallel: **side effects** — the baseline, the untouched items, and the consistency of every data shape the change touches across its other producers and consumers — and **intent match** — the build read back, per numbered success criterion, against `origin.md`, the snapshot of the ticket or 기획서 the intent gate bound, listing what is covered, missing, and beyond. A finding from any lens enters the build fix loop: three rounds, then the human, and `tools/auto.sh` reports an exhausted loop as `fixloop.exhausted`. ### Failed runs leave knowledge diff --git a/VERSION b/VERSION index bf057db..fd2726c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.10.0 +v0.11.0 diff --git a/docs/automation.md b/docs/automation.md index 29ac6d3..1d399d6 100644 --- a/docs/automation.md +++ b/docs/automation.md @@ -56,6 +56,7 @@ A shell script cannot review code, and this kit never claims one did. "blockers": [ { "code": "verify.fail", "detail": "…" } ], "gaps": [ { "code": "verify.unconfigured", "detail": "…" } ], "intent_contract": { "state": "ok", "detail": "…" }, + "fix_loop": { "state": "none", "detail": "…" }, "verification": { "state": "ok", "profile": "strict", "detail": "…", "receipt": "…" }, "source": { "state": "ok", "reviewed_digest": "…", "current_digest": "…" }, "delivery": { "state": "ok", "target": "pr", "detail": "…" }, @@ -178,8 +179,13 @@ environment: local instance, seeded fixture data check: build | build | npm run build check: R1 | unit | npm test -- login check: R2 | e2e | npx playwright test --grep @login +check: D1 | data | psql -Atc "select count(*) from sessions where token_v2 is null" | grep -qx 0 ``` +`data` is a read-only consistency query (the Side effects lens of +`roles/verifier.md`): it is receipted like every other check and never counts +as runtime evidence. + `tools/verify.sh run ` refuses an unfilled or malformed recipe outright, deletes any previous receipt, launches the runtime in its own process group, waits for the doctor, runs **every** configured check (stdin on `/dev/null`, one diff --git a/gates/_auto.sh b/gates/_auto.sh index 26839a9..ce4f90f 100644 --- a/gates/_auto.sh +++ b/gates/_auto.sh @@ -77,8 +77,7 @@ sdlc_auto_track() { # → compact | full if [ "$t" = compact ] && [ -f "$rec" ] && [ "$(sdlc_field "$rec" track || true)" != "compact" ]; then t=full; fi printf '%s\n' "$t" } -sdlc_auto_artifact_for() { case "$1" in - intent) echo intent.md;; spec) echo spec.md;; plan) echo plan.md;; ship) echo evidence.md;; esac; } +sdlc_auto_artifact_for() { sdlc_artifact_of "$1"; } # one map, in _common.sh # One stage's state: "|" # absent — no artifact yet @@ -101,7 +100,7 @@ sdlc_auto_stage_state() { # upart="$dir/$(sdlc_auto_artifact_for "$up")" [ -n "$upw" ] || continue if [ ! -f "$upart" ] || [ "$(sdlc_sha256_file "$upart" 2>/dev/null || true)" != "$upw" ]; then - echo "stale|$(sdlc_auto_artifact_for "$up") changed since the $stage approval — re-approve $up, then $stage"; return 0; fi + echo "stale|$(sdlc_auto_artifact_for "$up") changed since the $stage approval — $(sdlc_regate_hint "$up" "$stage")"; return 0; fi done for up in $(sdlc_upstream_unbound "$rec" "$slug"); do echo "stale|$(sdlc_auto_artifact_for "$up") is not part of the approved $stage basis — gates/approve.sh $stage $art"; return 0 @@ -230,6 +229,33 @@ EOF echo "ok|intent contract satisfied" } +# --- the build fix loop -------------------------------------------------------- +# deviations.md's `- round n/3:` lines are the counter (AGENTS.md rule 5, +# skills/4-build); a line's `re-check:` field is updated in place. Prints +# "|": +# none — no round recorded +# open — the latest round's re-check is pending or open, rounds remain +# resolved — the latest round's re-check is resolved +# exhausted — round 3's re-check is still open, or a round past the cap +# exists: a human decides, at every lazymode +sdlc_auto_fixloop_state() { # + local f=".sdlc/work/$1/deviations.md" n line + [ -f "$f" ] || { echo "none|no fix loop recorded"; return 0; } + { read -r n; read -r line; } </ { s=$0; sub(/^- *round /,"",s); sub(/\/.*/,"",s); if (s+0>=m) {m=s+0; l=$0} } + END{print m+0; print l}' "$f") +EOF + [ "${n:-0}" -ge 1 ] || { echo "none|no fix loop recorded"; return 0; } + [ "$n" -le 3 ] || { echo "exhausted|fix loop round $n recorded in deviations.md; the cap is 3 (skills/4-build) — the human decides"; return 0; } + case "$line" in + *"re-check: resolved"*) echo "resolved|fix loop round $n/3 re-check resolved";; + *"re-check: open"*) + if [ "$n" -ge 3 ]; then echo "exhausted|fix loop round 3/3 re-check still open — show the human the evidence and deviations.md (skills/4-build)" + else echo "open|fix loop round $n/3 re-check open — round $((n + 1)) next"; fi;; + *) echo "open|fix loop round $n/3 in progress";; + esac +} + # --- verification receipts ---------------------------------------------------- # The recipe is .sdlc/verify.md (templates/verify.md). Receipts are written by # tools/verify.sh from commands it executed itself, and every field a reader @@ -294,8 +320,8 @@ sdlc_verify_recipe_issue() { # [recipe] → "" when usable, else " | | '\''"; next } if (id ~ /[^a-zA-Z0-9._-]/) { bad = "id check id '\''" id "'\'' must be [a-zA-Z0-9._-]+ (it names a log file)"; next } - if (kind != "build" && kind != "unit" && kind != "lint" && kind != "runtime" && kind != "e2e") - { bad = "kind check '\''" id "'\'' has kind '\''" kind "'\'' — use build|unit|lint|runtime|e2e"; next } + if (kind != "build" && kind != "unit" && kind != "lint" && kind != "runtime" && kind != "e2e" && kind != "data") + { bad = "kind check '\''" id "'\'' has kind '\''" kind "'\'' — use build|unit|lint|runtime|e2e|data"; next } if (substr(cmd, 1, 1) == "<") { bad = "placeholder check '\''" id "'\'' still holds the template placeholder (" cmd ")"; next } if (seen[id]++) { bad = "duplicate two checks share the id '\''" id "'\''"; next } } @@ -375,7 +401,7 @@ EOF if [ "$csha" != "$(printf '%s' "$want" | sdlc_sha256_stdin)" ]; then echo "invalid|check '$id' was recorded for a different command than the recipe's — re-run tools/verify.sh run $slug"; return 0; fi case "$rc" in ''|*[!0-9]*) echo "invalid|check '$id' records no numeric exit status — re-run tools/verify.sh run $slug"; return 0;; esac - case "$kind" in build|unit|lint|runtime|e2e) ;; *) + case "$kind" in build|unit|lint|runtime|e2e|data) ;; *) echo "invalid|check '$id' records an unknown kind '$kind' — re-run tools/verify.sh run $slug"; return 0;; esac if [ ! -f "$log" ]; then echo "invalid|check '$id' cites a log that does not exist ($log) — re-run tools/verify.sh run $slug"; return 0; fi diff --git a/gates/_common.sh b/gates/_common.sh index b7b595e..2223387 100644 --- a/gates/_common.sh +++ b/gates/_common.sh @@ -26,6 +26,16 @@ sdlc_stage_artifact() { # → expected basename, or empty for an unknown *) return 1;; esac } +# Everything an upstream loop may bind: the gated artifacts plus origin.md, the +# snapshot of the ticket / 기획서 the request came from (templates/origin.md). +# The intent approval binds it, and so does every gate downstream; it is never +# a gate of its own — `origin` re-gates through intent. +sdlc_artifact_of() { case "$1" in origin) echo origin.md;; *) sdlc_stage_artifact "$1";; esac; } +sdlc_regate_of() { case "$1" in origin) echo intent;; *) echo "$1";; esac; } +sdlc_regate_hint() { # → "re-approve X, then Y" (just X when they coincide) + local r; r=$(sdlc_regate_of "$1") + if [ "$r" = "$2" ]; then echo "re-approve $r"; else echo "re-approve $r, then $2"; fi +} # Upstream artifacts whose content the gate also binds (AGENTS.md rule 3): a # material edit upstream must not leave a downstream gate reusable. These are # CANDIDATES: approve.sh binds the ones that exist at approval time, so the @@ -33,7 +43,7 @@ sdlc_stage_artifact() { # → expected basename, or empty for an unknown # and is never asked for an artifact it does not have. sdlc_upstream_stages() { # → stages listed oldest-first case "$1" in - intent) echo "";; spec) echo "intent";; plan) echo "intent spec";; ship) echo "intent spec plan";; + intent) echo "origin";; spec) echo "origin intent";; plan) echo "origin intent spec";; ship) echo "origin intent spec plan";; *) echo "";; esac } @@ -46,7 +56,7 @@ sdlc_upstream_unbound() { # → stage names, space-separated (ma stage=$(sdlc_field "$rec" stage || true) [ -n "$stage" ] || return 0 for up in $(sdlc_upstream_stages "$stage"); do - upart=".sdlc/work/$slug/$(sdlc_stage_artifact "$up")" + upart=".sdlc/work/$slug/$(sdlc_artifact_of "$up")" [ -f "$upart" ] || continue [ -n "$(sdlc_field "$rec" "upstream_$up" || true)" ] && continue out="$out $up" diff --git a/gates/approve.sh b/gates/approve.sh index 7661e30..c57460c 100755 --- a/gates/approve.sh +++ b/gates/approve.sh @@ -199,7 +199,7 @@ digest=$(sdlc_sha256_file "$canon") [ -n "$spelling" ] && echo "track_spelling: $spelling" || true # upstream binding: whatever this gate was approved ON TOP of for up in $(sdlc_upstream_stages "$stage"); do - upart=".sdlc/work/$slug/$(sdlc_stage_artifact "$up")" + upart=".sdlc/work/$slug/$(sdlc_artifact_of "$up")" if [ -f "$upart" ]; then echo "upstream_$up: $(sdlc_sha256_file "$upart")"; fi done if [ "$stage" = ship ]; then diff --git a/gates/autotest.sh b/gates/autotest.sh index 17f7c38..3701bf3 100755 --- a/gates/autotest.sh +++ b/gates/autotest.sh @@ -1094,6 +1094,43 @@ else pass "A23 not applicable: tools/_run.py cannot run at all without python3" fi +# ===================================================================== +# A24 the build fix loop cap is machine-readable: deviations.md's round lines +# decide, and an exhausted loop is a human decision at lazymode 4 +# ===================================================================== +P="$FIX/a24"; mkproj "$P" 4; cd "$P" +write_intent "$P" feat-a24 +gate approve.sh intent .sdlc/work/feat-a24/intent.md --lazy --review "read app.sh" >/dev/null +assert_exit_msg "A24a no fix loop recorded: build is ready" 0 "ready build" auto next feat-a24 +DV=.sdlc/work/feat-a24/deviations.md +printf '# Deviations: feat-a24\n## Fix loop\n- round 1/3: E2E · accepted F1 · declined none · re-check: open: F1 still fails\n' > "$DV" +assert_exit_msg "A24b an open round below the cap keeps build ready" 0 "ready build" auto next feat-a24 +printf -- '- round 2/3: E2E · accepted F1 · declined none · re-check: open: F1 still fails\n- round 3/3: E2E · accepted F1 · declined none · re-check: open: F1 still fails\n' >> "$DV" +assert_exit_msg "A24c round 3 still open is fixloop.exhausted: needs-human, lazymode 4 included" 10 "fixloop.exhausted" auto next feat-a24 +out=$(gate status.sh feat-a24 2>&1) +case "$out" in *"FIX LOOP EXHAUSTED"*) pass "A24d the cockpit says the same";; *) fail "A24d cockpit hides the exhausted loop" "$out";; esac +printf '# Deviations: feat-a24\n## Fix loop\n- round 1/3: E2E · accepted F1 · declined none · re-check: open: F1 still fails\n- round 2/3: E2E · accepted F1 · declined none · re-check: open: F1 still fails\n- round 3/3: E2E · accepted F1 · declined none · re-check: resolved\n' > "$DV" +assert_exit_msg "A24e round 3 resolved in place: build is ready again" 0 "ready build" auto next feat-a24 +printf -- '- round 4/3: E2E · accepted F2 · declined none · re-check: pending\n' >> "$DV" +assert_exit_msg "A24f a round past the cap is exhausted whatever its re-check says" 10 "fixloop.exhausted" auto next feat-a24 +out=$(auto status --json) +case "$out" in *'"fix_loop": {"state": "exhausted"'*) pass "A24g the machine view carries fix_loop";; *) fail "A24g fix_loop missing from JSON" "$out";; esac +printf '# Evidence: feat-a24\n- R1: sh app.sh → hello\n' > .sdlc/work/feat-a24/evidence.md +assert_exit_msg "A24h evidence.md over an exhausted loop does not make the lazy ship gate ready" 10 "fixloop.exhausted" auto next feat-a24 + +# ===================================================================== +# A25 `data` is a receipted check kind: a read-only consistency query runs and +# is recorded, and never counts as runtime evidence +# ===================================================================== +P="$FIX/a25"; mkproj "$P" 4; cd "$P" +write_intent "$P" feat-a25 +write_recipe "$P" advisory "check: D1 | data | sh -c 'test 1 -eq 1'" +assert_exit_msg "A25a a data check runs under the recipe" 0 "VERIFY ok" verify run feat-a25 +assert_grep .sdlc/work/feat-a25/verify-receipt.md '^check: D1 | data | 0 |' "A25b the receipt records the data check" +assert_grep .sdlc/work/feat-a25/verify-receipt.md '^runtime_evidence: no' "A25c a data check is not runtime evidence" +write_recipe "$P" advisory "check: D1 | data | sh -c 'test 1 -eq 2'" +assert_exit_msg "A25d a failing data check fails the run" 1 "VERIFY fail" verify run feat-a25 + echo echo "================================================================" echo "PASSED: $PASSED FAILED: $FAILED" diff --git a/gates/check-gate.sh b/gates/check-gate.sh index 7efcf85..8641c8a 100755 --- a/gates/check-gate.sh +++ b/gates/check-gate.sh @@ -34,11 +34,11 @@ now=$(sdlc_sha256_file "$canon") [ "$now" = "$recorded_digest" ] || closed "$canon changed after it was approved. Show the human what changed, then re-approve: gates/approve.sh $stage $canon" for up in $(sdlc_upstream_stages "$stage"); do - upart=".sdlc/work/$slug/$(sdlc_stage_artifact "$up")" + upart=".sdlc/work/$slug/$(sdlc_artifact_of "$up")" want=$(sdlc_field "$rec" "upstream_$up" || true) [ -n "$want" ] || continue [ -f "$upart" ] || closed "$upart was part of the approved '$stage' basis and is now missing. Re-approve $stage after restoring it." - [ "$(sdlc_sha256_file "$upart")" = "$want" ] || closed "$upart changed after '$stage' was approved — the downstream gate no longer covers what the human approved. Re-approve $up, then $stage." + [ "$(sdlc_sha256_file "$upart")" = "$want" ] || closed "$upart changed after '$stage' was approved — the downstream gate no longer covers what the human approved. $(sdlc_regate_hint "$up" "$stage")." done # The ship approval binds the reviewed SOURCE as well as evidence.md, and @@ -66,7 +66,7 @@ fi # nobody re-approved. Compact features have no spec.md/plan.md and are untouched. unbound_up=$(sdlc_upstream_unbound "$rec" "$slug") if [ -n "$unbound_up" ]; then - closed "the '$stage' approval of '$slug' binds no digest for $(for u in $unbound_up; do printf '%s ' "$(sdlc_stage_artifact "$u")"; done)— those artifacts exist but were never part of the approved basis (an older kit's record, or written after the approval). Re-approve: gates/approve.sh $stage $canon" + closed "the '$stage' approval of '$slug' binds no digest for $(for u in $unbound_up; do printf '%s ' "$(sdlc_artifact_of "$u")"; done)— those artifacts exist but were never part of the approved basis (an older kit's record, or written after the approval). Re-approve: gates/approve.sh $stage $canon" fi echo "GATE OPEN: $stage (approved @ $(sdlc_field "$rec" approved_at))" diff --git a/gates/close.sh b/gates/close.sh index fee254a..220409d 100755 --- a/gates/close.sh +++ b/gates/close.sh @@ -150,7 +150,7 @@ EOF # .sdlc/, which the source snapshot excludes, so this is the only check that # sees them. Compact features have no spec.md or plan.md: nothing is demanded. for up in $(sdlc_upstream_stages ship); do - upart="$dir/$(sdlc_stage_artifact "$up")" + upart="$dir/$(sdlc_artifact_of "$up")" upw=$(sdlc_field "$srec" "upstream_$up" || true) [ -n "$upw" ] || continue if [ ! -f "$upart" ]; then @@ -161,13 +161,13 @@ EOF fi if [ "$(sdlc_sha256_file "$upart")" != "$upw" ]; then echo "BLOCKED: $upart changed after the ship review — the approval no longer covers what the human approved." - echo " Show the human what changed, re-approve $up, then: gates/approve.sh ship $ev" + echo " Show the human what changed, re-approve $(sdlc_regate_of "$up"), then: gates/approve.sh ship $ev" exit 1 fi done unbound_up=$(sdlc_upstream_unbound "$srec" "$slug") if [ -n "$unbound_up" ]; then - echo "BLOCKED: the ship approval for '$slug' binds no digest for$(for u in $unbound_up; do printf ' %s' "$(sdlc_stage_artifact "$u")"; done)." + echo "BLOCKED: the ship approval for '$slug' binds no digest for$(for u in $unbound_up; do printf ' %s' "$(sdlc_artifact_of "$u")"; done)." echo " Those artifacts exist but were never part of the approved basis (an older" echo " kit's record, or written after the approval), so a rewrite would ride along." echo " Re-run the ship review, then: gates/approve.sh ship $ev" diff --git a/gates/e2e.sh b/gates/e2e.sh index 4a9e0ad..96b9446 100755 --- a/gates/e2e.sh +++ b/gates/e2e.sh @@ -296,6 +296,13 @@ mkdir -p .sdlc/work/fix-empty-query/scratch n=$(wc -l < .sdlc/work/fix-empty-query/scratch/repro-before.log | tr -d ' ') [ "$n" = 3 ] && pass "B1 bug reproduced before any change (empty query returned $n lines)" \ || fail "B1 bug did not reproduce (got $n lines, expected 3)" +cat > .sdlc/work/fix-empty-query/origin.md <<'EOF' +# Origin: fix-empty-query +- Ref: QA-7 (fixture ticket) +- Read with: fixture +## Text +An empty search must print nothing. +EOF cat > .sdlc/work/fix-empty-query/intent.md <<'EOF' # Intent: fix-empty-query - Goal: an empty search box no longer dumps every record. @@ -307,6 +314,7 @@ cat > .sdlc/work/fix-empty-query/intent.md <<'EOF' - [ ] `./app.sh search ""` prints nothing; `./test_app.sh` passes EOF assert_ok "B2 intent approved (fixture human)" sdlc approve.sh intent .sdlc/work/fix-empty-query/intent.md --delegated +assert_grep .sdlc/approvals/fix-empty-query.intent.approval '^upstream_origin: [0-9a-f]' "B2b the intent approval binds the origin snapshot" assert_fail_msg "B3 spec gate closed before its own approval" "GATE CLOSED" \ sdlc check-gate.sh spec .sdlc/work/fix-empty-query/spec.md cat > .sdlc/work/fix-empty-query/spec.md <<'EOF' @@ -412,6 +420,15 @@ assert_fail_msg "B10g a plan.md deleted after the ship review blocks 'shipped'" cp "$FIX/plan.keep" .sdlc/work/fix-empty-query/plan.md assert_ok_msg "B10h the restored upstream chain reopens the ship gate" "GATE OPEN" \ sdlc check-gate.sh ship .sdlc/work/fix-empty-query/evidence.md +assert_grep .sdlc/approvals/fix-empty-query.ship.approval '^upstream_origin: [0-9a-f]' \ + "B10i the full-route ship approval binds the origin snapshot" +cp .sdlc/work/fix-empty-query/origin.md "$FIX/origin.keep" +echo "- also export every record (ticket edited after the review)" >> .sdlc/work/fix-empty-query/origin.md +assert_fail_msg "B10j an origin.md edited after the ship review blocks 'shipped'" \ + "origin.md changed after the ship review" sdlc close.sh fix-empty-query shipped "delivered" +cp "$FIX/origin.keep" .sdlc/work/fix-empty-query/origin.md +assert_ok_msg "B10k the restored origin reopens the ship gate" "GATE OPEN" \ + sdlc check-gate.sh ship .sdlc/work/fix-empty-query/evidence.md cat > .sdlc/work/fix-empty-query/delivery.md </dev/null git clone -q "$B" "$FIX/clone-full" assert_file "$FIX/clone-full/.sdlc/archive/fix-empty-query/spec.md" "B13 spec.md survives a fresh clone" diff --git a/gates/selftest.sh b/gates/selftest.sh index c9469ff..09bfd30 100755 --- a/gates/selftest.sh +++ b/gates/selftest.sh @@ -186,6 +186,26 @@ mv .sdlc/work/feat-d/intent.md.orig .sdlc/work/feat-d/intent.md "$kit/gates/check-gate.sh" spec .sdlc/work/feat-d/spec.md >/dev/null || { echo "FAIL: spec gate not open after the upstream was restored"; exit 1; } echo "ok: upstream edit closes the downstream gate" +# 14b. origin.md (the ticket / 기획서 snapshot) is bound by the intent gate and +# every gate downstream of it, and is never a gate of its own +mkdir -p .sdlc/work/feat-o +echo "ticket A20-1: users can export" > .sdlc/work/feat-o/origin.md +echo i > .sdlc/work/feat-o/intent.md; echo s > .sdlc/work/feat-o/spec.md +"$kit/gates/approve.sh" intent .sdlc/work/feat-o/intent.md --delegated >/dev/null +grep -q '^upstream_origin: [0-9a-f]' .sdlc/approvals/feat-o.intent.approval || { echo "FAIL: intent approval does not bind origin.md"; exit 1; } +"$kit/gates/approve.sh" spec .sdlc/work/feat-o/spec.md --delegated >/dev/null +echo "edited after approval" >> .sdlc/work/feat-o/origin.md +out=$("$kit/gates/check-gate.sh" spec .sdlc/work/feat-o/spec.md 2>&1) && { echo "FAIL: spec gate survived an origin.md rewrite"; exit 1; } +case "$out" in (*"origin.md changed after"*"re-approve intent, then spec"*) ;; (*) echo "FAIL: origin-drift message wrong: $out"; exit 1;; esac +if "$kit/gates/approve.sh" origin .sdlc/work/feat-o/origin.md --delegated >/dev/null 2>&1; then + echo "FAIL: 'origin' accepted as a gate"; exit 1; fi +mkdir -p .sdlc/work/feat-o2; echo i > .sdlc/work/feat-o2/intent.md +"$kit/gates/approve.sh" intent .sdlc/work/feat-o2/intent.md --delegated >/dev/null +echo "late snapshot" > .sdlc/work/feat-o2/origin.md +out=$("$kit/gates/check-gate.sh" intent .sdlc/work/feat-o2/intent.md 2>&1) && { echo "FAIL: intent gate survived an origin.md written after the approval"; exit 1; } +case "$out" in (*"binds no digest for origin.md"*) ;; (*) echo "FAIL: unbound-origin message wrong: $out"; exit 1;; esac +echo "ok: origin.md is bound by intent and every downstream gate, never a gate itself" + # 15. tripwire.sh: flags risky plans, stays quiet on clean ones tw=.sdlc/work/feat-d/tw.md printf 'step 1: run ALTER TABLE users\nstep 2: edit Dockerfile\n' > "$tw" diff --git a/gates/status.sh b/gates/status.sh index b7b4807..5297f38 100755 --- a/gates/status.sh +++ b/gates/status.sh @@ -62,8 +62,7 @@ fi # stage order and the artifact each gate locks stages="intent spec plan ship" -artifact_for() { case "$1" in - intent) echo "intent.md";; spec) echo "spec.md";; plan) echo "plan.md";; ship) echo "evidence.md";; esac; } +artifact_for() { sdlc_artifact_of "$1"; } # one map, in _common.sh next_hint() { case "$1" in intent) echo "skills/2-spec";; spec) echo "skills/3-plan";; plan) echo "skills/4-build then 5-ship";; ship) echo "commit per skills/5-ship discipline, record delivery.md, then close.sh shipped";; esac; } @@ -176,7 +175,7 @@ for dir in .sdlc/work/*/; do [ -n "$upw" ] || continue if [ ! -f "$upart" ] || [ "$(sdlc_sha256_file "$upart" 2>/dev/null || true)" != "$upw" ]; then state="$state — STALE: $(artifact_for "$up") changed since approval" - [ -z "$next_action" ] && next_action="upstream $(artifact_for "$up") changed — re-approve $up, then $stage" + [ -z "$next_action" ] && next_action="upstream $(artifact_for "$up") changed — $(sdlc_regate_hint "$up" "$stage")" fi done # an upstream artifact on disk that this record binds with nothing (older @@ -244,6 +243,17 @@ EOF fi;; esac fi + # the build fix loop cap (skills/4-build), in the machine view's words: an + # exhausted loop is a human decision at every lazymode and overrides the next + # action, exactly like an open material question. + if [ ! -f ".sdlc/approvals/${slug}.ship.approval" ]; then + fst=$(sdlc_auto_fixloop_state "$slug") + case "${fst%%|*}" in + exhausted) printf " %-8s %s — %s\n" "build" "FIX LOOP EXHAUSTED" "${fst#*|}" + next_action="${fst#*|}";; + open|resolved) printf " %-8s %s\n" "build" "${fst#*|}";; + esac + fi # shipped means delivered (AGENTS.md rule 6): after the ship gate the feature # still owes a delivery record before close.sh will accept 'shipped'. if [ -f ".sdlc/approvals/${slug}.ship.approval" ]; then diff --git a/init.sh b/init.sh index 1423e28..b51b3b7 100755 --- a/init.sh +++ b/init.sh @@ -54,8 +54,8 @@ ensure_line .gitignore '.sdlc/archive/*/progress.md' ensure_line .gitignore '.sdlc/approvals/' ensure_line .gitignore '.sdlc/archive/*/approvals/' -# per-feature working residue. The durable record is intent.md, spec.md, -# plan.md, map.md, delivery.md, evidence.md, CLOSED — they are the reason the +# per-feature working residue. The durable record is origin.md, intent.md, +# spec.md, plan.md, map.md, delivery.md, evidence.md, CLOSED — they are the reason the # feature can be understood a year later, so they stay committed (AGENTS.md # rule 7). Only the bulky, machine-regenerable residue is ignored — including # the automation layer's two working files: checkpoint.md (pending execution diff --git a/log/changelog-v0.11.0.md b/log/changelog-v0.11.0.md new file mode 100644 index 0000000..6c386be --- /dev/null +++ b/log/changelog-v0.11.0.md @@ -0,0 +1,34 @@ +# v0.11.0 — three-lens verification, bound origin, machine-readable fix loop + +The build stage's verification grows from one E2E pass into three lenses that +run in parallel, each in a fresh context, under the one existing role +contract; the request's origin becomes a bound artifact; the fix-loop cap and +data-consistency checks become machine-readable. The gates and the source +binding are unchanged in kind. + +## Changes + +- **Three verification lenses.** `roles/verifier.md` is now the single definition of what verification checks, dispatched one lens each: **E2E** (the change through the real interface; the bug-fix proof chain; the `verify.sh` receipt), **Side effects** (baseline and untouched items, neighbouring flows, and data consistency — every shape the diff writes or reads followed to its other producers and consumers), and **Intent match** (the build read back, per intent.md O-item, against the origin of the request, listing what is covered, missing, and beyond; a detail dropped between the origin and an approved spec.md is a finding). The verifier now receives intent.md and its origin on the full route too — before, it saw only plan.md and spec.md, so the human's actual ask was never read back. +- **`origin.md` — the ticket / 기획서 as requested, bound by the gates** (templates/origin.md). Stage 1 snapshots the origin BEFORE the intent gate; `approve.sh intent` binds its digest exactly like a spec or plan upstream (`upstream_origin:`), and every downstream gate, `status.sh`, `check-gate.sh`, `tools/auto.sh` and `close.sh` report a rewrite in the same words (`origin.md changed after … — re-approve intent, then `). An origin written after the approval reads as unbound and closes the gate, exactly like a late spec.md. `origin` is never a gate of its own: `approve.sh origin` is refused. Absent when the request had no origin beyond the chat — nothing is demanded then. It is part of the durable record (AGENTS.md rule 7, init.sh, README trees). One artifact map now serves every script (`_common.sh` `sdlc_artifact_of`); `status.sh` and `_auto.sh` dropped their private copies. +- **O-numbered success criteria.** intent.md's Success criteria are `O1..On` in the origin's words (else the human's); spec R-items cite them (`R1: … (O1)`); an O-item with no R is a flagged concern, never a silent drop. The adversary's traceability check and the verifier's Intent match lens count coverage over them instead of rediscovering it. +- **Data touched in plan.md.** The plan names every shape the changed files write or read, with its other producers and consumers and what happens to pre-existing records. The plan adversary checks the list is complete; the Side effects lens executes it and treats a missed shape as a finding. +- **Fix loop, any lens, machine-readable cap.** `skills/4-build` routes a finding from ANY lens into the fix loop. A round is one deviations.md line — lens, accepted, declined, `re-check: pending | resolved | open: ` updated in place. The cap stays at three rounds. `gates/_auto.sh` `sdlc_auto_fixloop_state` reads those lines: round 3 still `open`, or any round past 3, is `fixloop.exhausted` — `tools/auto.sh next` exits 10 (needs-human) at every lazymode, both while build is open and once evidence.md exists over it (the lazy ship gate is refused), `status --json` carries `"fix_loop"`, and `status.sh` prints `FIX LOOP EXHAUSTED` and overrides its next action, exactly like an open material question. A finding that implies new scope is a human decision, not a fix. +- **`data` check kind** in `.sdlc/verify.md` (templates/verify.md, docs/automation.md): a read-only consistency query for the Side effects lens, receipted like every other check and never counted as runtime evidence. +- `templates/evidence.md` carries the three reports under one **Verification** section (E2E · Side effects · Intent match · Fix loop) and names the origin and its live re-read; the old End-to-end and Regression sections fold into it. +- DRY: the real-E2E prose that was repeated in AGENTS.md rule 6, `skills/4-build`, `skills/5-ship` and `roles/verifier.md` now lives in the role file; rule 6 states the contract in one paragraph and the stage skills reference it. + +## Upgrade notes + +- Nothing is required. Existing approvals keep working: a feature without origin.md binds nothing new, and existing evidence.md files stay valid — no script parses their section names. +- Existing `deviations.md` round lines without a `re-check:` field read as `open` (in progress) and never as exhausted; only a round past 3, or a round 3 marked `open`, blocks. +- Snapshot the ticket or 기획서 as `origin.md` BEFORE `approve.sh intent`; written afterwards it closes the gate as unbound until intent is re-approved — that is the binding working. +- `status --json` gained `fix_loop`; drivers that ignore unknown fields are unaffected. + +## Validation + +Local, on macOS (Bash 3.2) — darwin 25.6.0, arm64: + +- `bash gates/selftest.sh` → `SELFTEST PASS` (new: origin.md bound by intent and downstream gates, refused as a gate, unbound when written late). +- `bash gates/e2e.sh` → `E2E PASS`, 146 assertions, 0 failures (new: B2b, B10i–B10k, B12b — the full route snapshots, binds, refuses a post-review origin edit, archives origin.md). +- `bash gates/autotest.sh` → `AUTOTEST PASS`, 195 assertions, 0 failures (new: A24a–h fix-loop states through `next`, `status.sh`, `status --json` and the lazy ship gate; A25a–d the `data` kind runs, is receipted, is not runtime evidence, and fails the run when it fails). +- `bash -n` over every changed shell script: clean. diff --git a/roles/adversary.md b/roles/adversary.md index 873bc64..be0afda 100644 --- a/roles/adversary.md +++ b/roles/adversary.md @@ -9,8 +9,9 @@ plan.md as applicable) and `.sdlc/memory/POLICY.md` when it exists. Attack, in order: -1. **Traceability.** Does every element trace to the upstream artifact? Flag - added features and dropped requirements or questions. +1. **Traceability.** Does every element trace to the upstream artifact — + every spec R to an intent O-item, every O-item to an R or a flagged + concern? Flag added features and dropped requirements or questions. 2. **Domain and data shapes.** Check that schemas, contracts, migrations, and serialized data use the same shapes end to end. 3. **User claims.** Do `[assumed]` claims carry enough risk to block? Does the @@ -20,7 +21,9 @@ Attack, in order: 5. **Testability.** Can a machine check each requirement? Flag statements such as "works well" that do not name an observable result. 6. **For plans**: every spec requirement maps to a proof command; the file - list and work order are complete; risks reflect DOMAIN.md constraints; and + list and work order are complete; **Data touched** names every shape the + changed files write or read, with its other producers and consumers; + risks reflect DOMAIN.md constraints; and the **Gate tier** verdict is correct — re-check every trip-wire yourself (migration, data deletion, public API, security paths, infra/config, beyond-spec scope). `tools/tripwire.sh` output, when provided, is evidence diff --git a/roles/verifier.md b/roles/verifier.md index 5929341..023f2b4 100644 --- a/roles/verifier.md +++ b/roles/verifier.md @@ -1,89 +1,104 @@ # Role: Verifier (fresh context; use this file and the given paths only) -Review the change independently. **Report only. Fix no source.** - -Inputs you receive: paths to `plan.md`, `spec.md`, the changed-file list, and -`.sdlc/config.md` (real build/test/run commands). - -Do: - -1. Run the build, test, and lint commands from config.md. Record exact commands - and verdict output. If the project has a `.sdlc/verify.md` recipe, run - `tools/verify.sh run `: it executes the configured commands and writes - a receipt bound to this source. The receipt records what ran; YOUR report is - still the judgement, and a receipt never substitutes for it. -2. **Exercise the change for real, end to end** — the section below. Unit tests - are not a substitute for it and never stand in for it silently. -3. Check each `plan.md` **Proof** item (compact route: intent.md's Proof - line): does the promised evidence actually pass? -4. Brownfield: rerun the baseline commands; diff against `baseline.txt`. -5. Bug fixes: check the proof chain (AGENTS.md rule 6) yourself — reproduce - the ORIGINAL failure against the pre-fix state if you can (`git show - :` into a scratch copy, a disposable checkout of your own, or - the recorded capture; never `git stash` or anything else that mutates the - human's working tree), confirm the mechanism explains it, run the SAME - reproduction after, and exercise the flows that share the changed code. A - chain you cannot complete is a FAIL reason, or a stated limitation for an - intermittent defect — never a pass by assumption. - -## Real end-to-end check (scoped, not the whole suite) - -The point is that the change was observed working the way a user or caller -meets it — through the real interface, not through a mock of it: - -- **UI change** → drive the actual screen with the `qa:` tool in config.md (or - any browser/QA tool your harness has): load the page, do the user's steps, - read the rendered result. -- **API / CLI / job change** → issue the real request or command against a - running instance (the `run:` command in config.md is the usual way to start - one) and read the final response, exit status, and any resulting state. -- **Bug fix** → the SAME failing flow, before and after, plus the neighbouring - flows that actually share the changed code or data. Name them; do not invent - a fixed number of them, and do not check unrelated flows to fill a quota. - -Scope it to the change: the flows it touches, not every flow in the product, -and not the project's entire E2E suite on every pass. Reuse the project's own -commands and fixtures (config.md, the repo's e2e/test scripts) — never invent -a parallel harness. - -Record four things for each check: **command or tool · environment · scenario · -observed result** (the deciding output, verbatim; bulk into `scratch/`). - -**If the environment is missing, the answer is NOT VERIFIED.** No runnable app, -no browser tool, no reachable API, no credentials you are allowed to use: say -exactly that, name what is missing, and report the item as NOT VERIFIED. Do not -claim a pass, and do not quietly substitute unit tests for the real run. A -delivery may still go ahead if the human accepts it — but only as an explicitly -stated known gap in evidence.md, never as a silent one. - -Report format: +Check the change independently through ONE of the three lenses below — the +dispatcher names yours; the lenses run in parallel, fresh context each. +**Report only. Fix no source.** + +Inputs: intent.md and origin.md (the snapshot of the ticket / 기획서 the +intent gate bound; absent when the request had no origin beyond the chat), +spec.md and plan.md (compact route: intent.md only), the changed-file list, +`.sdlc/config.md` commands, and `baseline.txt` when it exists. + +## Lens 1 — E2E: does the change work where the user meets it? + +1. Run the build, test, and lint commands from config.md; record exact + commands and verdict lines. With a `.sdlc/verify.md` recipe, run + `tools/verify.sh run `: the receipt records what ran, your report is + still the judgement. +2. Exercise the change for real, scoped to it — the flows it touches, not the + product's whole suite; the project's own commands and fixtures, never a + parallel harness: + - **UI** → drive the actual screen (`qa:` tool, else any browser tool in + the harness): load it, do the user's steps, read the rendered result. + - **API / CLI / job** → the real request or command against a running + instance (`run:`); read the response, exit status, and resulting state. + - **Bug fix** → the proof chain (AGENTS.md rule 6): reproduce the ORIGINAL + failure against the pre-fix state (`git show :` into scratch + or a disposable checkout — never `git stash` or anything that mutates the + human's tree), confirm the mechanism, run the SAME reproduction after. + A chain you cannot complete is a FAIL, or a stated limitation for an + intermittent defect — never a pass by assumption. +3. Check each plan.md **Proof** item (compact route: intent.md's Proof line). + +## Lens 2 — Side effects: what else changed between AS-IS and TO-BE? + +Assume the feature works and look for what it broke, skewed, or left behind: + +1. Brownfield: rerun the baseline commands and diff against `baseline.txt`; + check every "stays untouched" item (spec.md U-items) and the neighbouring + flows that share the changed code or data. Name them; no quota. +2. **Data consistency.** Start from plan.md's **Data touched** list (compact + route: intent.md's Risk line) and add any shape the diff touches that it + missed — a missed shape is itself a finding. Follow each one to its other + producers and consumers: records that predate the change + (missing or default values), derived copies (caches, denormalized columns, + search indexes, exports, reports), jobs and consumers still reading the old + shape, migrations that leave rows half-converted. Query real data where a + read-only tool exists. +3. Every AS-IS → TO-BE pair in spec.md observed as written — plus any pair the + spec did not list but the code now changes. + +## Lens 3 — Intent match: is this what was actually asked for? + +Read origin.md, then re-read the live ticket / 기획서 with the project's tool +when it is reachable: text that differs from the snapshot is a finding (the +request moved after the approval). Then, per intent.md O-item and per detail +the origin text names — screens, fields, messages, roles, limits, error cases: + +1. **Covered** — O-item → the spec R that carries it → where the build shows + it (E2E observation or code path). +2. **Missing** — an O-item or origin detail absent from the build. A detail + dropped between the origin and spec.md is a finding even though spec.md was + approved without it. +3. **Beyond** — behavior no O-item asked for. +4. intent.md's Goal line, checked the same way. + +No origin.md → check O-items alone and report `origin NOT VERIFIED — none +snapshotted`; live source unreachable → say so, the snapshot stands. + +## Report + +Record every check as **command or tool · environment · scenario · observed +result** (deciding lines verbatim; bulk into `.sdlc/work//scratch/`). +**No environment = NOT VERIFIED**: name what is missing (no runnable app, no +browser tool, no reachable API, no permitted credentials). Never a pass, and +never unit tests standing in for the real run; delivering over the gap is the +human's explicit call, recorded in evidence.md. ``` -## Verifier report +## Verifier report — (fill your lens's lines) - Ran: -- E2E: · · - (or: NOT VERIFIED — ) +- E2E: · · +- Bug proof (fixes): before · mechanism · after - Proof items: pass / fail (list failures) -- Bug proof (fixes only): before · mechanism · after · neighbouring flows -- Baseline diff: clean | differences: -- Mismatches vs plan/spec: -VERDICT: PASS | FAIL (reasons) | PASS WITH GAP () +- Baseline diff: clean | differences: · untouched: · neighbouring flows: +- Data consistency: → consistent | skew: +- AS-IS → TO-BE: · unlisted changes: +- Origin: · live: unchanged | drifted: | unreachable · O1 → R1 → … · Covered / · Missing: · Beyond: +VERDICT: PASS | FAIL (findings, each with evidence) | PASS WITH GAP () ``` -Do not dismiss a failure as acceptable. If a config.md command fails, report it -as a finding. +Do not dismiss a failure as acceptable. A failing config.md command is a +finding. Do not report a clean result after a shallow pass. Tools: -- Needs: shell (run/build/test commands from `.sdlc/config.md`), file reads. -- For UI changes, use the `qa:` tool named in config.md; when that line is - empty or absent, use any browser or QA tool available in your harness. Do - this when the app is reachable; otherwise record why the UI was not checked. - You may use a read-only database tool to check data claims. Name each tool - used. +- Needs: shell (config.md commands) and file reads; the project's ticket or + document tool for the origin; the `qa:` tool or any browser/QA tool for UI; + a read-only database tool for data claims. Name each tool used. - **Write authority**: you may NOT change source, tests, or any stage artifact. You MAY produce what running things produces — build output, test reports, - logs, screenshots, and your own disposable fixtures (a temp dir, a scratch - copy of a file, a local throwaway database) — and put anything bulky in - `.sdlc/work//scratch/`. Leave the human's working tree as you found it. + logs, screenshots, disposable fixtures (a temp dir, a scratch copy of a file, + a throwaway local database); bulky things go to `scratch/`. Leave the human's + working tree as you found it. - Must not: edit source or artifacts, use deploy/release tools, touch production systems or credentials. diff --git a/skills/1-intent/SKILL.md b/skills/1-intent/SKILL.md index de7ca9a..60c8aae 100644 --- a/skills/1-intent/SKILL.md +++ b/skills/1-intent/SKILL.md @@ -123,6 +123,14 @@ carries the fact to every later feature. ## Write the artifact +When the request has an origin — a ticket, a 기획서, an incident — snapshot +it FIRST as `.sdlc/work//origin.md` (templates/origin.md): the intent +approval binds it, so it is written before the gate, and an edit afterwards +closes the gates by design (an edited ticket is a new decision). Number the +success criteria `O1..On` in the origin's words (else the human's): spec +R-items cite them and the verifier's Intent match lens counts Covered/Missing +over them. + Fill `templates/intent.md` → `.sdlc/work//intent.md`. Every claim labeled. Questions are carried forward in two sections, and the split matters: diff --git a/skills/2-spec/SKILL.md b/skills/2-spec/SKILL.md index c224b8a..64636e3 100644 --- a/skills/2-spec/SKILL.md +++ b/skills/2-spec/SKILL.md @@ -36,8 +36,9 @@ Fill `templates/spec.md`. Rules: one-line decision with your recommendation. Test: would a non-developer colleague understand every sentence? If not, rewrite. Write it LAST (after the adversarial pass), place it FIRST. -- Every requirement traces to a line in intent.md. Do not add features that - intent.md does not request. +- Every requirement cites the intent.md O-item it fulfils (`R1: … (O1)`). An + O-item with no R becomes a flagged concern, never a silent drop; a feature no + O-item asks for is not added. - Every intent.md open question ends up in exactly one of two places: answered in the spec, or carried forward as a flagged concern. - Define data shapes before behavior. Check schemas, API contracts, migrations, diff --git a/skills/3-plan/SKILL.md b/skills/3-plan/SKILL.md index 21d093d..d7b0f56 100644 --- a/skills/3-plan/SKILL.md +++ b/skills/3-plan/SKILL.md @@ -29,6 +29,10 @@ Explore the codebase. Use a fresh-context researcher for large areas so raw exploration stays out of the main context. Then fill `templates/plan.md`: - **Files that change.** Give exact paths and mark each as new or modified. +- **Data touched.** Every shape those files write or read — table, column, + type, event, file — with its other producers and consumers and what happens + to records that predate the change. The adversary checks it is complete; + the verifier's Side effects lens executes it. - **Order of work.** Make each step keep the configured checks passing. Add tests with the code they test. - **Risks.** Record rate limits, migrations, shared state, and important quirks. diff --git a/skills/4-build/SKILL.md b/skills/4-build/SKILL.md index e8ae624..693063e 100644 --- a/skills/4-build/SKILL.md +++ b/skills/4-build/SKILL.md @@ -91,52 +91,48 @@ Order of work — and update it before each dispatch (AGENTS.md rule 9). Stage 5, not the build step. When your harness can restrict subagent tools, copy each role's "Must not" list into the dispatch. -## Verify (fresh context, every time) +## Verify (fresh context, three lenses in parallel) When all steps are done and the full configured suite is green over the final -state, dispatch a fresh-context verifier (`roles/verifier.md`) with: plan.md, -spec.md (compact route: intent.md), the changed-file list, and -`.sdlc/config.md` commands (full dispatch contract per AGENTS.md rule 5). It -runs the app/tests itself and reports evidence. You do not verify your own -work in your own context. If the harness cannot give it a fresh context, say -so as an explicit gap in evidence.md ("no independent verification available: -") — never a quiet self-review. - -The verifier's job includes the **real end-to-end check** (AGENTS.md rule 6): -the changed behavior driven through the real screen, request, or command — -scoped to this change, with the project's own commands (`.sdlc/config.md` -`e2e:`, `qa:`, `run:`) — recorded as command/tool · environment · scenario · -observed result. No runnable environment means NOT VERIFIED, named as such; -the configured unit suite is not a stand-in for it. The verifier changes no -source and no artifact, but it may produce build/test output, logs, and its -own disposable fixtures. - -When the project has a `.sdlc/verify.md` recipe (templates/verify.md), run -`tools/verify.sh run ` as part of that pass: it launches what the checks -need, waits for the doctor, runs the configured build/unit/lint/runtime/e2e -commands, and records a receipt bound to this source, the recipe, and each -command's and output's digest. The receipt makes a check that never ran, a log -edited afterwards, or a result that belongs to other code DETECTABLE — it is -change detection, not authentication, not the verifier's judgement, and no -substitute for the fresh context. Quote its deciding lines in evidence.md; a code or recipe change -makes it `stale` and it must be re-run. - -For a bug fix, the verifier's job includes the proof chain (AGENTS.md rule 6): -the failure reproduced before the fix, the causal mechanism, the SAME -reproduction passing after, and the neighbouring flows through the changed -code. - -Verifier or adversary findings enter the **fix loop**: - -1. Mark every finding **accepted** or **declined**. Give a reason for each - declined finding. Record both lists — and the round number — in - deviations.md; ship copies them into evidence.md. -2. Fix only accepted findings. Do not add unrelated scope. A - finding that implies new scope goes to the human, not into the fix. -3. Dispatch a new fresh-context checker with two questions. Is each named - finding resolved? Did the fix create a defect in affected code? -4. Stop after three rounds. If round 3 still fails, show the evidence to the - human. Do not run a fourth round without new information. +state, dispatch `roles/verifier.md` three times — one lens each, fresh context +each, in parallel where the harness allows (AGENTS.md rule 5; full dispatch +contract) — with intent.md and its `Refs:` origin, plan.md and spec.md (compact +route: intent.md only), the changed-file list, `.sdlc/config.md`, and +`baseline.txt` when it exists: + +1. **E2E** — the change works through the real interface; a bug fix carries + its proof chain (AGENTS.md rule 6). +2. **Side effects** — AS-IS → TO-BE beyond the requirement: baseline, untouched + items, data consistency across every producer and consumer of the shapes + the diff touches. +3. **Intent match** — the build read back against the ticket or 기획서 the + request came from: covered, missing, beyond. + +You do not verify your own work in your own context; a harness that cannot +give a fresh context records the gap in evidence.md ("no independent +verification available: "). A lens with no environment reports NOT +VERIFIED, never a pass. With a `.sdlc/verify.md` recipe the E2E lens runs +`tools/verify.sh run `; quote the receipt's deciding lines in evidence.md +and re-run it after any code or recipe change (it goes `stale`). + +A finding from any lens — a failing flow, a data skew, a missing origin +detail — enters the **fix loop**: + +1. Mark every finding **accepted** or **declined**, with a reason for each + declined one. Write the round line in deviations.md now — lens, both lists, + `re-check: pending` (templates/deviations.md); ship copies it into + evidence.md. +2. Fix only accepted findings. A finding that implies new scope — an origin + detail intent.md never carried, a data model change — goes to the human as + a decision, not into the fix. +3. Re-dispatch the lenses that had findings, plus E2E whenever code changed, + with two questions: is each named finding resolved, and did the fix create + a defect in affected code? Update the round line's `re-check:` in place: + `resolved`, or `open: ` and the next round begins. +4. **Cap: three rounds.** A round-3 re-check still `open` STOPs the loop: + show the human the evidence and the deviations.md trail. No round 4 — + `tools/auto.sh` reads the round lines and reports either as + `fixloop.exhausted`, needs-human at every lazymode. ## Exit diff --git a/skills/5-ship/SKILL.md b/skills/5-ship/SKILL.md index 727e4d3..aa5e93a 100644 --- a/skills/5-ship/SKILL.md +++ b/skills/5-ship/SKILL.md @@ -47,35 +47,20 @@ Fill `templates/evidence.md` → `.sdlc/work//evidence.md`: - For each spec requirement, include the exact command and real output. Keep every numerical result. For long successful logs, include the verdict lines and numbers and cite the full scratch output. Include all failure output. -- For brownfield work, compare baseline and after using the same commands. - Check each "stays untouched" item. +- **The three verifier reports** (roles/verifier.md — E2E, Side effects, + Intent match) as reported, each check as command/tool · environment · + scenario · observed result, with the fix-loop rounds from deviations.md. A + lens that reported NOT VERIFIED stays NOT VERIFIED here: name what is + missing; unit tests never stand in, and delivering over the gap is the + human's explicit call, recorded under Not verified. +- **Bug fixes: the proof chain** (AGENTS.md rule 6) in the Bug proof section. + A chain with a missing link is a diagnosis, not a confirmed fix: label it + that way here and in the report. - Include full test, lint, and build results. Long successful logs may use the same verdict-lines-and-scratch-citation rule. - Adversary findings + resolutions. - State anything not verified, including environment limits and skipped checks. Record a gap instead of marking the check as passed. -- **The real end-to-end run, scoped to this change** (roles/verifier.md): the - changed behavior exercised through the interface a user or caller actually - meets — the real screen for a UI change (the `qa:` tool from config.md, or - any browser/QA tool in the harness), a real request or command against a - running instance for an API/CLI/job change, and for a bug fix the SAME - failing flow before and after plus the neighbouring flows that share the - changed code. Record command or tool · environment · scenario · observed - result. Reuse the project's own commands; do not build a parallel harness, - and do not re-run the project's entire E2E suite as a ritual. - **A missing environment means NOT VERIFIED** — name what is missing and say - so here. Unit tests never stand in for the real run, and a green suite is - not a substitute. Delivering anyway is possible only as an explicit known - gap the human accepted, recorded in this file. -- For every AS-IS to TO-BE pair, record the observed result and its command or - browser evidence. -- **Bug fixes: the proof chain** (AGENTS.md rule 6), in evidence.md's Bug - proof section — the failure observed BEFORE the fix, the causal mechanism - that explains it, the SAME reproduction passing after, and the adjacent - flows through the changed code. An intermittent defect may substitute - logs, traces, or an isolated deterministic reproduction, with its - limitation named. A chain with a missing link is a diagnosis, not a - confirmed fix: label it that way here and in the report. ## Retrospective @@ -143,7 +128,7 @@ approval is not that authorization. — when changed — `.sdlc/memory/POLICY.md` and `.sdlc/config.md` (lazymode, command, and `qa:` edits must reach the audit trail). Do not use `git add -A` or `git add .` because they can include unrelated files. - Staging `.sdlc/work//` yields the durable record — `intent.md`, + Staging `.sdlc/work//` yields the durable record — `origin.md`, `intent.md`, `spec.md`, `plan.md`, `map.md`, `evidence.md`, `delivery.md`; approvals, harvest.md, deviations.md, baseline.txt, progress.md, and scratch/ are gitignored (init.sh) and stay on disk. If any of THOSE appears in the diff --git a/templates/deviations.md b/templates/deviations.md index 62c5eb7..795afd6 100644 --- a/templates/deviations.md +++ b/templates/deviations.md @@ -10,5 +10,5 @@ ## Re-gates - re-gate 1/2: -## Fix loop -- round 1/3: accepted · declined +## Fix loop +- round 1/3: · accepted · declined · re-check: pending | resolved | open: diff --git a/templates/evidence.md b/templates/evidence.md index bf8071e..04356d3 100644 --- a/templates/evidence.md +++ b/templates/evidence.md @@ -2,6 +2,7 @@ - From: plan.md (approved YYYY-MM-DD) - Diff: +- Origin: : unchanged | drifted: | unreachable> -- Command/tool: -- Environment: -- Scenario: -- Observed: -- Neighbouring flows: -- NOT VERIFIED: - -## Regression -- Baseline vs after: -- U1: +## Verification +### E2E +- · · +### Side effects +- Baseline vs after: · U1: · neighbouring flows: +- Data consistency: +- Unlisted changes: +### Intent match +- O1 → R1 → · O2 → → MISSING: +- Covered: / · Missing: · Beyond: +### Fix loop +- round 1/3: · accepted · declined · re-check: resolved | open: ## Full checks - Build: `` → @@ -43,7 +45,7 @@ - round 2/2: ## Not verified - + ## Retro lessons - → harvest.md [promote: skills/ if applicable] diff --git a/templates/intent.md b/templates/intent.md index 41c128c..871d6bd 100644 --- a/templates/intent.md +++ b/templates/intent.md @@ -6,7 +6,8 @@ - Type: greenfield | brownfield - Track: full (default) | compact — - Requested by: -- Refs: +- Refs: - Scope authorization: | waived-by-human | waived-by-agent — unreproduced, diagnosis stays [assumed] --> - verification debt: -## Success criteria - -- [ ] +## Success criteria + +- [ ] O1: ## Compact route + +- Ref: +- Read with: +- Snapshot: YYYY-MM-DDTHH:MM:SSZ + +## Text + diff --git a/templates/plan.md b/templates/plan.md index 949680b..3945639 100644 --- a/templates/plan.md +++ b/templates/plan.md @@ -18,6 +18,12 @@ Policy: AGENTS.md rule 3.> ## Files that change - (new | modified): +## Data touched + +- · written | read · also produced/consumed by: · pre-existing records: + ## Order of work 1. diff --git a/templates/spec.md b/templates/spec.md index bc29ce6..7e794a4 100644 --- a/templates/spec.md +++ b/templates/spec.md @@ -15,8 +15,9 @@ should be able to approve the spec from this section and Flagged concerns alone.> ## Requirements - -- R1: (intent: "") + +- R1: (O1) ## Data shapes diff --git a/templates/verify.md b/templates/verify.md index 77009ff..56d068c 100644 --- a/templates/verify.md +++ b/templates/verify.md @@ -51,9 +51,11 @@ environment: # check: | | # id [a-zA-Z0-9._-]+ — usually the requirement id (R1, R2, …). It names # the log file under .sdlc/work//scratch/verify/.log. -# kind build | unit | lint | runtime | e2e +# kind build | unit | lint | runtime | e2e | data # `runtime` and `e2e` are the only kinds that count as the real run: # the change driven through the interface a user or caller meets. +# `data` is a READ-ONLY query that proves a consistency claim (the +# Side effects lens, roles/verifier.md) — receipted, never the real run. # command the project's OWN command, scoped to the change where possible. # It runs with stdin on /dev/null, in its own process group, bounded # by check_timeout. EVERY configured check runs, and the receipt @@ -63,3 +65,4 @@ check: unit | unit | check: lint | lint | check: R1 | e2e | check: R2 | runtime | +check: D1 | data | diff --git a/tools/auto.sh b/tools/auto.sh index fd11ff2..ce333d2 100755 --- a/tools/auto.sh +++ b/tools/auto.sh @@ -102,6 +102,7 @@ evaluate() { # EV_NEXT_KIND=""; EV_NEXT_CMD=""; EV_NEXT_TEXT="" EV_SRC_STATE=""; EV_SRC_WANT=""; EV_SRC_NOW="" EV_VERIFY=""; EV_VERIFY_DETAIL=""; EV_INTENT=""; EV_INTENT_DETAIL="" + EV_FIXLOOP=""; EV_FIXLOOP_DETAIL="" EV_DELIVERY=""; EV_DELIVERY_DETAIL=""; EV_DELIVERY_TARGET=""; EV_HANDOFF="" EV_REMOTE=""; EV_BRANCH=""; EV_REMOTE_SHA=""; EV_REMOTE_VERDICT="not-checked" EV_TRACK=$(sdlc_auto_track "$s") @@ -112,6 +113,7 @@ evaluate() { # st=$(sdlc_auto_intent_contract "$s"); EV_INTENT="${st%%|*}"; EV_INTENT_DETAIL="${st#*|}" st=$(sdlc_verify_state "$s"); EV_VERIFY="${st%%|*}"; EV_VERIFY_DETAIL="${st#*|}" + st=$(sdlc_auto_fixloop_state "$s"); EV_FIXLOOP="${st%%|*}"; EV_FIXLOOP_DETAIL="${st#*|}" if [ -f "$dir/CLOSED" ]; then EV_STAGE=closed; EV_STATUS=complete @@ -145,7 +147,12 @@ evaluate() { # # evidence.md missing means the work itself is still open: build and # the independent verification come before the ship artifact EV_STAGE=build - case "$EV_VERIFY" in + if [ "$EV_FIXLOOP" = exhausted ]; then + # the fix loop hit its cap (skills/4-build): a human decides, at every + # lazymode — no verification state makes this 'ready' + EV_STATUS=needs-human; add_blocker fixloop.exhausted "$EV_FIXLOOP_DETAIL" + set_next human "" "$EV_FIXLOOP_DETAIL" + else case "$EV_VERIFY" in fail|invalid) EV_STATUS=blocked; add_blocker verify.fail "$EV_VERIFY_DETAIL" set_next verify "tools/verify.sh run $s" "the verification is not satisfied over this source — fix it, then re-run";; blocked) EV_STATUS=blocked; add_blocker verify.environment "$EV_VERIFY_DETAIL" @@ -160,7 +167,7 @@ evaluate() { # set_next build "" "build and verify per skills/4-build + roles/verifier.md, then write $art";; ok) EV_STATUS=ready set_next write "" "write $art (templates/evidence.md), quoting the receipt's deciding lines";; - esac + esac; fi else EV_STAGE="$stage"; EV_STATUS=ready set_next write "" "write $art (see $(skill_dir_for "$stage"))" @@ -198,6 +205,12 @@ evaluate() { # unconfigured) add_gap verify.unconfigured "$EV_VERIFY_DETAIL";; esac fi + if [ "$stage" = ship ] && [ "$EV_FIXLOOP" = exhausted ]; then + # evidence.md written over an exhausted fix loop: the ship gate is the + # human's, whatever the lazymode (AGENTS.md rule 3, blocker past its cap) + EV_STATUS=needs-human; add_blocker fixloop.exhausted "$EV_FIXLOOP_DETAIL" + set_next human "gates/approve.sh ship $art" "$EV_FIXLOOP_DETAIL"; return 0 + fi if [ "$LAZY" -ge "$(sdlc_auto_lazy_min "$stage")" ]; then EV_STATUS=ready set_next approve "gates/approve.sh $stage $art --lazy --review \"\"" \ @@ -373,6 +386,8 @@ feature_json() { # printf ' "gaps": '; emit_blockers_json "$EV_GAPS"; printf ',\n' printf ' "intent_contract": {"state": %s, "detail": %s},\n' \ "$(sdlc_json_str "$EV_INTENT")" "$(sdlc_json_str "$EV_INTENT_DETAIL")" + printf ' "fix_loop": {"state": %s, "detail": %s},\n' \ + "$(sdlc_json_str "$EV_FIXLOOP")" "$(sdlc_json_str "$EV_FIXLOOP_DETAIL")" printf ' "verification": {"state": %s, "profile": %s, "detail": %s, "receipt": %s},\n' \ "$(sdlc_json_str "$EV_VERIFY")" "$(sdlc_json_str "$(sdlc_verify_profile)")" \ "$(sdlc_json_str "$EV_VERIFY_DETAIL")" "$(sdlc_json_str "$(sdlc_verify_receipt "$s")")" From fff5cb1fac55fb07a46f5199a3a3a7e661eb30db Mon Sep 17 00:00:00 2001 From: cskwork Date: Thu, 17 Sep 2026 21:53:58 +0900 Subject: [PATCH 2/2] fix: read SKILL.md as UTF-8 in the selftest frontmatter check (Windows cp1252) --- gates/selftest.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gates/selftest.sh b/gates/selftest.sh index 09bfd30..4b8971c 100755 --- a/gates/selftest.sh +++ b/gates/selftest.sh @@ -117,7 +117,7 @@ if [ -n "$py" ]; then import sys, glob, os failed = [] for p in glob.glob(os.path.join(sys.argv[1], '**/SKILL.md'), recursive=True): - text = open(p).read() + text = open(p, encoding='utf-8').read() # SKILL.md is UTF-8; Windows defaults to cp1252 if not text.startswith('---'): failed.append(f'{p}: no frontmatter'); continue fm = text.split('---')[1]