diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index befc1337..7b8220d9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -60,7 +60,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + with: + fetch-depth: 2 # HEAD^1 = base side of the PR merge commit (or previous main on push) - run: python3 scripts/check-worklog-json.py + env: + WORKLOG_BASE: HEAD^1 - run: python3 scripts/check-ledgers-frozen.py # a conflict resolution that drops one side's work leaves no other trace: no marker, mergeable diff --git a/AGENTS.md b/AGENTS.md index 80b192b5..8f602c54 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,6 +73,16 @@ as written because it records decisions that were made at the time. Any other key (`schema`, `taskId`, `summary`, `changes`, `verification`, `issues`, …) is free — the consumer does not read them, so they are for humans and the next round. +**Proposal threshold (2026-09-25).** 09-14~24 this repo left 63 proposals, and each adoption bred +0.98 new ones — 62% of them meta, longest chain 11 generations. So: +- Ask otterpebble's three questions first (`.claude/rules/autonomy.md` §추천도 3문): observed this + round · runnable now without waiting on anything · not a duplicate. Any "no" ⇒ one line in the reply, not a card. +- **Zero proposals is the normal value.** At most **2 per worklog** — the lock fails a new or changed `.json` with 3+. +- A proposal in the **3rd or later generation of a chain** (adopting a card that itself came from an adopted card) is not filed if it is meta. +- Optional `proposals[i].kind`: `"product"` = a user- or product-visible result changes · `"meta"` = checker, + guard, census, self-test, ratchet, ledger/doc rule, CI wiring. Absent = unclassified; do not backfill old + worklogs. Tower builds its card badge and default filter from it. The lock rejects any other value. + **No retroactive conversion.** The convention applies to new rounds only; the lock asks only "if a `.json` exists, is it well-formed and does it have its `.md` sibling" — it never demands a `.json` for an existing `.md`. Lock: `scripts/check-worklog-json.py`, run by the `worklog_json` diff --git a/docs/worklog/2026-09-25-worklog-proposal-threshold-and-kind.md b/docs/worklog/2026-09-25-worklog-proposal-threshold-and-kind.md new file mode 100644 index 00000000..4504b20a --- /dev/null +++ b/docs/worklog/2026-09-25-worklog-proposal-threshold-and-kind.md @@ -0,0 +1,4 @@ +## [2026-09-25] worklog 제안 문턱 + `kind` 필드 (rustjava-worklog-proposal-threshold-and-kind) +- 무엇을: `AGENTS.md` 에 제안 문턱(추천도 3문 참조 · 0개가 정상 · worklog 당 최대 2 · 체인 3대째 meta 금지)과 선택 필드 `proposals[i].kind`(`product`|`meta`)를 넣고, `check-worklog-json.py` 가 `kind` 값과 «새/변경 worklog 의 제안 3개 이상»을 red 로 잡게 했다. 과거 파일은 base 와 바이트 동일하면 통과(CI 는 `WORKLOG_BASE=HEAD^1` · fetch-depth 2). +- 왜: 09-14~24 제안 63건 · 채택 1건당 새 제안 0.98 · meta 62% · 최장 체인 11대 — 입구에 문턱이 없었다. +- 사용자 영향: 없음(문서·검사기만). 후속 제안 없음. diff --git a/scripts/check-worklog-json.py b/scripts/check-worklog-json.py index 6fcf0549..f8d522a1 100755 --- a/scripts/check-worklog-json.py +++ b/scripts/check-worklog-json.py @@ -13,7 +13,9 @@ """ import json +import os import re +import subprocess import sys from pathlib import Path @@ -25,6 +27,23 @@ PROPOSAL_KEYS = ("title", "plainSummary", "userBenefit", "why", "tradeoff", "effort", "target") DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}") +KINDS = ("product", "meta") +MAX_PROPOSALS = 2 # AGENTS.md "Proposal threshold" + +# The cap binds only new or changed files: a past worklog identical to the base passes (no retroactive edits). +# CI sets WORKLOG_BASE=HEAD^1 (the PR merge commit's base side); locally the default is the fork point from origin/main. +BASE = os.environ.get("WORKLOG_BASE") or subprocess.run( + ["git", "merge-base", "HEAD", "origin/main"], cwd=ROOT, capture_output=True, text=True +).stdout.strip() + + +def at_base(p): + if not BASE: + return None + r = subprocess.run(["git", "show", f"{BASE}:{p.relative_to(ROOT).as_posix()}"], cwd=ROOT, capture_output=True) + return r.stdout if r.returncode == 0 else None + + fail = [] @@ -58,6 +77,9 @@ def check(cond, msg): for k in PROPOSAL_KEYS: v = prop.get(k) check(isinstance(v, str) and v.strip(), f"{p.name}#p{i}: '{k}' missing — that field renders empty on the card") + check("kind" not in prop or prop["kind"] in KINDS, f"{p.name}#p{i}: kind {prop.get('kind')!r} — must be one of {KINDS} or absent") + if len(proposals) > MAX_PROPOSALS and at_base(p) != p.read_bytes(): + fail.append(f"{p.name}: {len(proposals)} proposals in a new/changed worklog — the cap is {MAX_PROPOSALS} (AGENTS.md \"Proposal threshold\")") for key in ("adoptedProposals", "declinedProposals"): refs = obj.get(key, []) @@ -74,5 +96,7 @@ def check(cond, msg): for m in fail: print(f"WORKLOG-JSON: {m}", file=sys.stderr) -print(f"checked {len(list(WORKLOG.glob('*.json')))} worklog .json file(s), {len(fail)} problem(s)") +if not BASE: + print("WORKLOG-JSON: no git base resolved — every file counts as changed for the proposal cap", file=sys.stderr) +print(f"checked {len(list(WORKLOG.glob('*.json')))} worklog .json file(s) against base {BASE[:12] or '-'}, {len(fail)} problem(s)") sys.exit(1 if fail else 0)