Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
@@ -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대 — 입구에 문턱이 없었다.
- 사용자 영향: 없음(문서·검사기만). 후속 제안 없음.
26 changes: 25 additions & 1 deletion scripts/check-worklog-json.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"""

import json
import os
import re
import subprocess
import sys
from pathlib import Path

Expand All @@ -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 = []


Expand Down Expand Up @@ -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, [])
Expand All @@ -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)
Loading