KilimcininKorOglu/claude-code-mods/tree/main/plugins/contract-watch
contract-watch
모델이 Edit로 함수 시그니처를 바꾸면 ripwire가 찾은 호출자를 Edit 결과에 추가해 빌드 전에 모델이 수정하도록 합니다.
이 mod 소개
contract-watch
모델이 함수에 매개변수를 추가하면 편집은 멀쩡해 보여도 다른 곳의 호출자 3개가 깨집니다. 이 mod는 편집 시 문제를 잡습니다. Edit가 함수 매개변수를 바꾸면 ripwire에 호출자를 묻고 Edit 결과에 추가합니다.
하는 일
- Edit 도구를 감시합니다. 성공 후 old_string과 new_string의 한 줄 함수 정의를 비교합니다. Go의 func, JS와 TS 함수, 화살표 함수와 클래스 메서드, Python의 def, Rust의 fn, Java 메서드와 PHP 함수를 대상으로 합니다.
- 두 문자열이 함수를 정의하지만 매개변수가 다르면 시그니처 변경입니다. 본문만 바꾸는 편집은 아무것도 하지 않습니다.
- 변경마다 argv로 ripwire <repo root> --edit-check=<file>:<name>을 실행해 정의를 git HEAD와 비교하고 호출자를 나열합니다.
- status=contract-change이면 메모를 읽습니다: contract-watch: parse changed from 1 to 2 parameter(s) since the last commit; check each caller: main (main.go:5), other (main.go:9). 최대 10개를 표시하고 incompatible=1인 호출자를 먼저 둡니다. contract-watch: parse changed from 1 to 2 parameter(s) since the last commit; these callers do not match the new arity: main (main.go:5). Other callers of that name, which the call graph binds by name and may belong to another type: other (lib.go:9). Check each. Messaging::sendAlert와 SNMP_Monitor::sendAlert처럼 이름이 같은 메서드도 모두 남깁니다.
- 동시에 결과만 트랜스크립트에 한 줄로 기록합니다: contract-watch: parse changed from 1 to 2 parameter(s); do not match: main (main.go:5); same name: other (lib.go:9).
- sidebar를 열면 변경, 표시된 호출자, 같은 이름의 호출자와 (file:line)이 스트림에 나타납니다. 없으면 트랜스크립트에 기록됩니다. 메모는 모든 호출자를 포함하며 Go 검사에서 incompatible=0인데도 두 호출자가 인자 하나를 전달한 사례(ripwire 2.1.278)도 남깁니다.
- mod는 양쪽 모드에서 발견을 보관하고 다음 git commit, git push, git merge 전에 다시 측정합니다. 모두 맞으면 contract-watch: every caller matches parse again, 계약은 달라도 표시가 없으면 contract-watch: no caller of parse carries the mismatch mark any more를 표시합니다. 측정은 명령 전이며 이전 인자 수의 호출자가 남아 있으면 발견은 열려 있습니다.
- 각 메인 루프 턴 끝에 남은 발견을 측정해 다음 프롬프트와 함께 전달합니다: contract-watch: 1 changed signature(s) still leave a caller behind: parse changed from 1 to 2 parameter(s), 1 caller(s) do not match. Bring each caller to the new signature, or take the signature change back.
- deny 모드는 호출자가 남으면 명령을 멈춥니다. incompatible 수가 0보다 큰 검사만 게이트를 멈춥니다. git commit은 인덱스(저장소마다 한 번의 git diff --cached --name-only -z)로 파일을 좁히고 push와 merge는 모두 계산합니다. 우회는 없고 /contract-watch mode note로 게이트를 끕니다. note가 기본입니다.
Command
/contract-watch on or off, the mode, and the signatures that leave a caller behind
/contract-watch on | off on by default
/contract-watch mode note note only; the default
/contract-watch mode deny a commit, a push and a merge also stop while a caller does not match
Install
claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install contract-watch@kilimcininkoroglu-mods
Function hooks are early access. Claude Code 2.1.288 and later load them by default.
After installing
- ripwire를 PATH에 둡니다. 없으면 the callers were not checked: ...가 표시되고 편집은 계속됩니다.
- Claude Code를 다시 시작합니다.
What it can reach
Validated with claude plugin validate on Claude Code 2.1.283:
./register.ts hooks: session.start, command.run{command=contract-watch}, turn.complete, prompt.submit, tool.call{tool=Bash}, tool.call{tool=Edit}
./register.ts calls: $.command.register, $.process.run (via askRipwire, locate, stagedIn), $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get (via isEnabled, readSettings), $.store.set (via runCommand, setMode), $.ui.log (via atGitCommand, toPerson)
Reach L2: 프로세스를 실행합니다.
Limits
- 한 줄 정의만 읽으며 여러 줄 시그니처는 보지 않습니다.
- 이름을 바꾼 함수는 비교하지 않습니다.
- git HEAD와 비교하므로 커밋 전 같은 함수를 다시 편집하면 메모를 반복합니다.
- Edit만 감시하며 전체 파일을 바꾸는 Write는 대상이 아닙니다.
- git 저장소 밖에서는 실행하지 않습니다.
- ripwire가 색인하지 않는 함수는 검사하지 않습니다.
- 게이트는 incompatible 수를 따릅니다.
- deny 모드에는 우회가 없습니다.
- 스크립트나 git commit을 숨기는 별칭을 통한 커밋은 막지 않습니다.
- git commit -a, -am, -- 뒤의 경로 지정은 인덱스로 좁히지 않고 모든 발견을 계산합니다.
Development
make install # eslint, typescript-eslint, typescript
make lint # complexity limit 10, the build fails above it
make typecheck # needs .claude/types/ from /plugin-types
make validate
make test # claude plugin test
설치
먼저 작성자의 README에서 marketplace와 플러그인 이름을 확인하세요. 저장소 구조에 따라 명령어가 달라질 수 있습니다.
claude plugin marketplace add KilimcininKorOglu/claude-code-mods claude plugin install contract-watch
원문 / README
contract-watch
The model adds a parameter to a function, the edit looks fine, and three callers elsewhere in the code are now broken; you find out when the build or a test fails. This mod catches it at the edit: when an Edit changes a function's parameters, it asks ripwire who calls that function and puts the callers right into the Edit's result.
What it does
-
It watches the Edit tool. After a successful edit it compares the one-line function definitions in
old_stringandnew_string: Gofunc, JS and TS functions, arrow functions and class methods, Pythondef, Rustfn, Java methods and PHP functions. -
A function that both strings define with different parameters is a changed signature. An edit that only touches a body runs nothing.
-
For each changed signature it runs
ripwire <repo root> --edit-check=<file>:<name>by argv. ripwire compares the definition with git HEAD and lists the callers. -
When ripwire reports
status="contract-change", the model reads this note right after the Edit's result:contract-watch: parse changed from 1 to 2 parameter(s) since the last commit; check each caller: main (main.go:5), other (main.go:9).Each caller is named with the definition it sits in; at most 10 are named and the rest are counted. When ripwire marks a caller
incompatible="1", every folded definition it sees disagrees with the new arity. Those callers come first, under a sentence of their own:contract-watch: parse changed from 1 to 2 parameter(s) since the last commit; these callers do not match the new arity: main (main.go:5). Other callers of that name, which the call graph binds by name and may belong to another type: other (lib.go:9). Check each.The second group matters in a codebase where several types define a method with the same name: the call graph binds a call by its name, so
Messaging::sendAlertreads the same asSNMP_Monitor::sendAlert. Neither group is dropped. -
At the same moment you get one line in the transcript, so you see what the model was told. It holds the finding alone, without the instruction:
contract-watch: parse changed from 1 to 2 parameter(s); do not match: main (main.go:5); same name: other (lib.go:9)The note and the line are separate channels: the model never reads the line, and you never read the note.
-
With the sidebar open, the finding goes into its stream instead and the transcript stays clean. The first line shows the change (the old parameter count faint, the new one yellow). Under it come the marked callers' names in red, then the same-named ones in faint text after a
same name, may be another typeline, each with its(file:line)faint. The entry stays until newer ones push it off the pane. Without the sidebar, the line lands in the transcript as above.
The note lists every caller, not only the ones ripwire proves incompatible: in a live check on Go, ripwire reported incompatible="0" while both callers still passed one argument (measured with ripwire on 2.1.278).
-
The mod holds every reported signature open and closes it itself, in both modes. At the next
git commit,git pushorgit mergethe model runs, and before that command runs, ripwire measures each open symbol again. A symbol that no caller misses any more closes with a green line, and its sidebar entry is dropped:contract-watch: every caller matches parse againThat line comes when the contract reads the same as the last commit again. When the contract still differs but no caller carries ripwire's
incompatiblemark any more, the closing line names that narrower measure instead, because a call graph that binds by name cannot prove every caller right:contract-watch: no caller of parse carries the mismatch mark any moreThe measurement runs before the command, not after it.
--edit-checkcompares the working tree against git HEAD, so once a commit has landed there is nothing left to compare and every finding would look closed. For the same reason an open symbol is held by ripwire's incompatible mark alone, not by the contract status: after a commit took the change, the contract reads as HEAD, while a caller left on the old arity still carries the mark, so the finding stays open at the turn's end until the mark is gone. -
A finding the model did not close is measured the same way at the end of each main-loop turn, and whatever is left reaches the model as one note with your next prompt. ripwire runs on this machine, once per open symbol:
contract-watch: 1 changed signature(s) still leave a caller behind: parse changed from 1 to 2 parameter(s), 1 caller(s) do not match. Bring each caller to the new signature, or take the signature change back.That is one note per turn, not one per prompt. Without it the finding would be said once, at the edit, and then sit in the pane while the model forgot about it. You read nothing new, because the pane already shows the same finding.
-
In
denymode that same moment also stops the command while a changed signature leaves a caller behind. The gate uses a narrower measure than the note: only a check whoseincompatiblecount is above zero holds it, meaning the callers ripwire names on fixed-arity evidence. Agit commitanswers for its own files alone: the mod reads the index (git diff --cached --name-only -z, once per repository) and lets the commit run when it holds none of the files those signatures live in, with one line telling you how many still stand. Apushand amergehave no index to read, so every finding counts there. There is no bypass; only you turn the gate off, with/contract-watch mode note.notemode is the default and stops nothing.
In the live check the model read the note after its Edit and said that the two callers would not compile until they were updated.
Command
/contract-watch on or off, the mode, and the signatures that leave a caller behind
/contract-watch on | off on by default
/contract-watch mode note note only; the default
/contract-watch mode deny a commit, a push and a merge also stop while a caller does not match
Install
claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install contract-watch@kilimcininkoroglu-mods
Function hooks are early access. Claude Code 2.1.288 and later load them by default, so there is nothing to switch on.
After installing
- Install ripwire and put it on PATH. Without it, a changed signature writes
the callers were not checked: ...as a yellow entry (a transcript line with the sidebar closed), once until a different error comes, and the edit goes through as before. - Restart Claude Code.
What it can reach
Validated with claude plugin validate on Claude Code 2.1.283:
❯ ./register.ts hooks: session.start, command.run{command=contract-watch}, turn.complete, prompt.submit, tool.call{tool=Bash}, tool.call{tool=Edit}
❯ ./register.ts calls: $.command.register, $.process.run (via askRipwire, locate, stagedIn), $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get (via isEnabled, readSettings), $.store.set (via runCommand, setMode), $.ui.log (via atGitCommand, toPerson)
Reach L2: it runs processes.
1. Reads: the old and new text of each Edit; the Bash command text; through ripwire, the repository's source and git HEAD
2. Runs: git rev-parse, git diff --cached --name-only -z and ripwire --edit-check, read-only, by argv, after an edit that changed a signature, and once per open symbol before a git commit, push or merge and at each turn's end
3. Sends: a note to the model after the Edit's result, one more with the next prompt while a finding stands, and one line to the transcript; nothing leaves the machine
4. Persists: in $.store, the on/off setting and the mode
5. Hostile input: a function name comes from the edited text and reaches ripwire as one argv item, never through a shell
Limits
- Only a definition on one line is read. A signature whose parameters span several lines is not seen.
- A renamed function is not checked: the old name is gone, so ripwire has nothing to compare.
- The comparison is against git HEAD. A second signature edit of the same function before a commit repeats the note.
- Only the Edit tool is watched. A Write that replaces a whole file is not.
- Outside a git repository nothing runs.
- A function ripwire does not index, such as a JavaScript function inside a PHP file's
<script>block, is not checked. The sidebar shows one faint line,<name>: ripwire does not index it, its callers were not checked, and the model reads nothing. An open symbol ripwire no longer indexes was removed or renamed, and its finding closes. - The gate follows ripwire's
incompatiblecount, which is itself a floor: a caller ripwire cannot bind by name does not hold the gate. The note remains the wider measure. - The
denymode has no bypass. When a finding cannot be fixed, you turn the gate off with/contract-watch mode note. - The gate reads the command text. A commit through a script or an alias that hides
git commitis not stopped; the finding is then measured at the next turn's end instead. - A
git commit -a, a-amand a commit with a pathspec after--are not narrowed to the index, because they commit files the index does not hold yet. Every open finding counts for those.
Development
make install # eslint, typescript-eslint, typescript
make lint # complexity limit 10, the build fails above it
make typecheck # needs .claude/types/ from /plugin-types
make validate
make test # claude plugin test
