protologs
Skill0 starsChanged 6 days agoMITView on GitHub
Injects temporary debug logs (tag PROTOLOG) into the vertical slice of the current diff to verify runtime behavior on device. Two modes: - /wk:protologs -> injects logs (asks to confirm base branch and, optionally, the entry point / root point where the flow starts), commits them as a [TEMP] commit so multiple rounds of logs and any fixes requested in between stay individually trackable. - /wk:protologs cleanup -> reverts every [TEMP] commit (sed removal is only a fallback), leaving the code identical to its original state, and verifies no trace remains. Also invoked by /wk:doer Stage 4 and /wk:bugfix Stage 6 for on-device runtime verification.
The file
Show its rules and text
We split this skill into the rules it gives an agent
25 rules. About 0 are common, 10 are distinctive, and 15 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈10
Distinctive · estimate
This author's own choices. Worth a look.
15
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 29
Every injected line carries the exact prefix PROTOLOG - (with trailing space).
- Very specific: line 47
Before Step 1, run "${CLAUDE_PLUGIN_ROOT}/lib/helpers/session.sh" start protologs.
- Very specific: line 76
If /wk:doer Stage 4 or /wk:bugfix Stage 6 invoked this skill and already supplied entry points (bugfix's entry_points[], or the AC/flow …
- Very specific: line 86
If the user gives entry points, store them verbatim as <ENTRY_POINTS> for Step 4.
- Very specific: line 89
If the user says derive (or gives nothing), set <ENTRY_POINTS> to none - derive the entry point from the diff and proceed exactly as before.
- Very specific: line 92
If <DIFF_FILES> is empty AND <ENTRY_POINTS> is also none - derive the entry point from the diff, narrate: *"No changes between the current …
- Very specific: line 96
If <DIFF_FILES> is empty but explicit entry points were given, do not stop.
- Distinctive: line 107
Before the agent modifies anything, capture the full pre-inject state.
- Distinctive: line 120
If cleanup fails for any reason, the user can restore the pre-inject state by resetting tracked files to HEAD and reapplying the …
- Distinctive: line 133
MUST invoke a general-purpose agent using the Agent tool.
- Very specific: line 476
NOT commit, do NOT fix, do NOT recompile.
- Distinctive: line 503
After the agent returns:
- Very specific: line 506
If <ENTRY_POINTS> were given in Step 2.5, echo them back next to each slice's entry: *"Entry point you gave: <ENTRY_POINTS>.
- Very specific: line 511
If there are gaps in any coverage, split them by reason and show BOTH groups:
- Very specific: line 542
Before Step 1, run "${CLAUDE_PLUGIN_ROOT}/lib/helpers/session.sh" start protologs (same idempotent call as inject.
- Distinctive: line 610
Run it as its own command, never piped: a pipe hides the exit code.
- Very specific: line 630
Only when the strict pass refuses AND the offending line is confirmed to be exactly the documented embedded shape (.also { …
- Distinctive: line 655
Before anything else, confirm no revert is still in progress.
- Distinctive: line 662
If it exits 1 (prints something), STOP: do not verify, do not report cleanup as done.
- Very specific: line 684
Determine
<BASE>: run the same auto-detection as inject Step 1 (upstream tracking -> develop -> main -> master). - Distinctive: line 716
Run a last sanity check and narrate the result:
- Very specific: line 722
If the only modified files are those from git diff <BASE>...HEAD --name-only (the branch's intended changes, minus their PROTOLOG lines) -> …
- Distinctive: line 725
If unexpected files still appear -> show them and ask the user to decide.
- Very specific: line 752
If this cleanup run was invoked directly (/wk:protologs cleanup typed by the user), run "${CLAUDE_PLUGIN_ROOT}/lib/helpers/session.sh" stop.
- Distinctive: line 763
Never use em dashes in any response.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
protologs
Injects / cleans runtime logs on the current branch. Works standalone in any
repo; /wk:doer (Stage 4) and /wk:bugfix (Stage 6) invoke it for their
runtime verification, but it has no dependency on their state files.
How the tag works
Every injected line carries the exact prefix PROTOLOG - (with trailing space).
That tag is the only footprint left by the skill; deleting it is enough to fully
restore the files.
Filter on Android device:
adb logcat | grep "PROTOLOG - "Fallback: Android Studio Logcat with filter PROTOLOG - .
Mode: inject (default)
Activated when the user types /wk:protologs with no arguments, or when /wk:doer Stage 4 / /wk:bugfix Stage 6 invokes this skill in inject mode.
Before Step 1, run "${CLAUDE_PLUGIN_ROOT}/lib/helpers/session.sh" start protologs. This activates this plugin's PreToolUse guards for the session (they are inert otherwise, see lib/workspace-guard.md). Safe to run even when /wk:doer//wk:bugfix already started their own marker for this session; it is idempotent per session pid.
Step 1 - Confirm base branch
"${CLAUDE_PLUGIN_ROOT}/lib/helpers/git-checks.sh" base-candidatesPrints {"current": "<branch>", "candidates": [...]} (upstream tracking branch, then
develop/main/master, only the ones that actually exist locally, current branch and
duplicates excluded). Show the found candidates via AskUserQuestion with available
options plus "Other branch / type it yourself". If they choose "Other", ask a second
free-text AskUserQuestion. The chosen branch is used as <BASE> for the rest of the flow.
Step 2 - Compute the diff
"${CLAUDE_PLUGIN_ROOT}/lib/helpers/git-checks.sh" diff-files <BASE>Union of changed files (staged + unstaged + diff vs base), deduplicated, one per line,
into <DIFF_FILES>. Do not stop yet if this is empty; Step 2.5 decides whether an empty
diff is fatal. Exit 2 means the base is unusable or git failed: stop and report it, never read it as an empty or clean result.
Step 2.5 - Confirm entry point (root point)
Per lib/narration.md, the answer here is open free text, so this is a
plain-chat question, not AskUserQuestion.
If /wk:doer Stage 4 or /wk:bugfix Stage 6 invoked this skill and already
supplied entry points (bugfix's entry_points[], or the AC/flow context from
doer's plan), skip this question entirely and use what was supplied.
Otherwise ask in chat: "Where does the flow you want to verify start? Give
me entry points (file paths, class/function names, or a short flow
description, e.g. HomeFragment.onResume or "the checkout button tap").
Multiple are fine. Reply derive (or leave it empty) to let me infer the
entry point from the diff instead."
- If the user gives entry points, store them verbatim as
<ENTRY_POINTS>for Step 4. They are an anchor that complements the diff, not a replacement for it:<DIFF_FILES>is still used. - If the user says
derive(or gives nothing), set<ENTRY_POINTS>tonone - derive the entry point from the diffand proceed exactly as before. - If
<DIFF_FILES>is empty AND<ENTRY_POINTS>is alsonone - derive the entry point from the diff, narrate: "No changes between the current branch and<BASE>, and no entry point given. Nothing to instrument." and stop without touching any files. - If
<DIFF_FILES>is empty but explicit entry points were given, do not stop; proceed to instrument the slice starting from those entry points alone.
Show the file list (<DIFF_FILES>, may be empty when entry points carry the
slice) and any <ENTRY_POINTS> to the user and confirm via AskUserQuestion:
"I will instrument the vertical slice of these N files vs <BASE>[, anchored
at <ENTRY_POINTS>]. Continue?"
Step 3 - Safety backup in /tmp
Before the agent modifies anything, capture the full pre-inject state. The committed patch alone misses uncommitted work, so save the working tree and index too:
BRANCH=$(git rev-parse --abbrev-ref HEAD | tr '/' '-')
git diff <BASE>...HEAD > /tmp/protolog-committed-${BRANCH}.patch
git diff HEAD > /tmp/protolog-workdir-${BRANCH}.patch
git diff --cached > /tmp/protolog-staged-${BRANCH}.patchNarrate all three patch paths. /tmp/protolog-workdir-${BRANCH}.patch is the source
of truth for the pre-inject state: it captures every staged and unstaged change vs HEAD.
If cleanup fails for any reason, the user can restore the pre-inject state by resetting tracked files to HEAD and reapplying the working-tree patch:
git checkout -- .
git apply /tmp/protolog-workdir-<branch>.patchThe committed and staged patches are kept as references for the committed changes and the index snapshot respectively.
Step 4 - Inject logs via agent
MUST invoke a general-purpose agent using the Agent tool. The orchestrator does NOT inject logs inline.
Agent prompt (fill in the <...> markers):
You are the logger-agent. Your ONLY function is to add temporary debug lines
with the tag "PROTOLOG - " to a targeted diagnostic slice of the change
described below.
== INVIOLABLE RULES (read first, apply to every line you write) ==
VALID LINE SHAPES (only these two exist). Every PROTOLOG line occupies its own
full line, with nothing else on it, so that deleting it (by text match on
"PROTOLOG - ") leaves the surrounding code exactly as it was:
1. A standalone print call: `println("PROTOLOG - <message>")`.
2. A standalone `.also { println("PROTOLOG - <message>") }` continuing the
expression on the PREVIOUS line (Kotlin/Swift allow a leading-dot
continuation). This is the sanctioned way to log a return value, a `when`
branch result, or a constructor call WITHOUT refactoring anything: deleting
the line leaves the original expression syntactically complete.
```kotlin
// when branch:
FeatureEntity.HandlerType.PROMOTION_LIST_FOR_YOU -> PROMOTION_LIST_FOR_YOU
.also { println("PROTOLOG - [${Thread.currentThread().name}] FeaturesMapper: branch PROMOTION_LIST_FOR_YOU") }
// return / constructor:
return PromotionList.Item(
...
)
.also { println("PROTOLOG - [${Thread.currentThread().name}] PromotionListMapper: exit value=$it") }Never write the .also { println(...) } on the SAME line as the code it is
attached to (e.g. X -> Y.also { println(...) } or ).also { println(...) }
glued to a closing paren). That mixes a PROTOLOG line with business logic on
one physical line; deleting it by text match deletes the logic with it. This
is exactly the bug that broke a when block and two constructor calls in a
real incident. The .also {} ALWAYS goes on its own line below.
FORBIDDEN:
- Creating a variable ONLY to print its value. BAD: val endpoint = if (x) "a" else "b" println("PROTOLOG - endpoint=$endpoint") GOOD: println("PROTOLOG - endpoint=${if (x) "a" else "b"}")
- Refactoring existing code to enable a log. Do not split returns, method chains,
or long expressions. If a log would require a refactor, find the NEAREST
previous or subsequent point where you CAN log without changing logic.
NEVER skip that slice node entirely.
BAD: val r = foo()
println("PROTOLOG - result=$r")
return r
GOOD: println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: about to call foo")
return foo()
ALSO GOOD (when the returned/emitted value itself needs logging, use the
standalone
.also {}form above, never inline it): BAD: return foo().also { println("PROTOLOG - result=$it") } GOOD: return foo() .also { println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: result=$it") } - Adding helpers, factories, or any extra function. The only permitted change
is println/print/console.log lines (standalone or standalone
.also {}). - Modifying existing business logic, imports, or comments.
- Mixing a PROTOLOG line with business logic on the same physical line. Every
.also { println("PROTOLOG - ...") }is on its own line (see VALID LINE SHAPES). NeverX -> Y.also { ... }, never a println joined by;to real code, never a println hanging off the line that closes a constructor call.
Every modified file must be such that deleting all lines containing "PROTOLOG - " restores it to a syntactically identical state to its exact prior state. When a log would require breaking one of these rules, log at the NEAREST legal point instead; never skip the slice node in silence and never bend a rule to fit it in.
A "gap" is a slice hop where it is TECHNICALLY IMPOSSIBLE to add a log without
violating the rules above (not merely difficult or inconvenient). The bar for
declaring a gap is high: exhaust all nearby alternatives first. This is
distinct from a hop skipped by the hop budget below (see SLICE SCOPE):
report each under its own reason in slice_coverage.gaps.
== SELF-CHECK BEFORE RETURNING (mandatory, before compiling) ==
Before you compile and before you return the JSON, run this against every file you touched, and fix anything it catches. A structural violation can compile clean and only break later at cleanup time, so catching it here (not after the orchestrator's own backstop) is what keeps a round mergeable.
python3 "${CLAUDE_PLUGIN_ROOT}/hooks/protolog-restore.py" check "<file>" <(git show "HEAD:<file>" 2>/dev/null || true)Exit 0: the file is clean, move on. Exit 1 (Check A: a non-PROTOLOG line was added or removed compared to HEAD, a forbidden refactor, FORBIDDEN #2): revert it with the Edit tool and re-run. Exit 2 (Check B: a PROTOLOG line is glued to real code or otherwise malformed, FORBIDDEN #5): move it to its own line per VALID LINE SHAPES and re-run.
Repeat until it passes, on every touched file, before moving on to
compilation. Report the outcome in the JSON's self_check field: "clean"
if it passed with nothing to fix, or the list of what you found and
corrected.
== Full diff ==
<output of git diff <BASE>...HEAD + git diff + git diff --cached>
== Files to instrument == <file list from Step 2, <DIFF_FILES>>
== USER-SPECIFIED ENTRY POINTS (authoritative) == <ENTRY_POINTS from Step 2.5, or "none - derive the entry point from the diff">
When entry points are given, they are the ROOT of the slice: start tracing there and follow the flow downward through the diff to the boundary. Do NOT substitute a different entry point you infer on your own; if the given entry point does not reach the diff, log both paths and report it as a gap.
== SLICE SCOPE (hop budget, not exhaustive) ==
Trace the vertical slice in BOTH directions, but under a budget:
ENTRY POINT the action that triggers the flow (tap, click, call, event, scheduled job, etc.) | downward through every function/method the call passes through, | following from the diff UPWARD to the entry point AND DOWNWARD | to the boundary. BOUNDARY the point where the process touches the outside world: network call, DB query, file system, IPC, coroutine handoff. | back up through the return path OBSERVABLE the visible result: UI rendered, state emitted, RESULT HTTP response, record persisted, event fired.
UPWARD budget: at most 3 hops from the function the diff touches up to the entry point. If USER-SPECIFIED ENTRY POINTS were given above, ignore the hop count and start there instead, since the entry point is authoritative and may sit further than 3 hops away.
DOWNWARD: stop at the first external boundary (network, DB, filesystem, IPC) or at the boundary of a third-party framework/SDK/library. Log the call into it and the result it returns; do not descend into its code.
Any hop that falls outside the upward budget is NOT a gap under the technical-
impossibility bar above: it is a scope cut. Report it in slice_coverage.gaps
with "reason": "hop budget" and the file:function where you stopped, so the
orchestrator can offer the dev an extra round anchored there. Do not silently
skip it without reporting.
== DENSITY (one level for the whole slice) ==
MANDATORY, for every function in the slice:
[ ] Entry to the function with the relevant arguments (name=value) [ ] Exit of the function (value returned or "returning void") [ ] Every branch actually taken (if/else/when/guard), with the condition and its value [ ] Every catch/exception: exception type and message
CONDITIONAL, only when the value sits on the data path being verified (it flows from or to the diff or the entry point, not incidental local state):
[ ] Collection size before/after an operation on it [ ] Whether a nullable is null or non-null at the point of use [ ] Before AND after a suspend fun call [ ] A StateFlow/SharedFlow/LiveData emission [ ] Which Result/Either/sealed class variant was received [ ] The value going into and coming out of a flow collect/map/filter
When a value is not on that path, do not log it. This is a change in kind from "log everything you can": skip anything off the verified path even if it would be easy to add.
== KOTLIN LINE FORMS ==
Use these forms wherever the DENSITY section above calls for a log. This is format, not an independent list of things to log.
Message format - include the current thread name in EVERY log line:
println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: description key=value")
When coroutine debug mode is on, the thread name shows as main @coroutine#N,
which is invaluable for tracing async flows. One println per site, no refactor.
Coroutines - log before AND after each suspend fun call (when on the verified path): println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: calling suspendFoo(arg=$arg)") val result = suspendFoo(arg) println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: suspendFoo returned=$result")
StateFlow/SharedFlow - when logging an emission: println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: emitting state=${newState}") _state.emit(newState)
When branch (mandatory, per DENSITY) - log each branch actually taken with the matched value: when (x) { is Foo -> { println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: when branch=Foo, x=$x") ... } is Bar -> { println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: when branch=Bar, x=$x") ... } }
Lists/collections - when logging a size, before operating: println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: list.size=${list.size}, isEmpty=${list.isEmpty()}")
Nullable - when logging null/non-null, inside each ?.let and ?: run: val x = maybeNull?.let { println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: maybeNull is non-null, value=$it") it.process() } ?: run { println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: maybeNull is NULL, using fallback") fallback() }
Data classes - when logging one, use toString() if <= 6 properties, or field by field if larger: println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: model=$model")
Result/sealed class (mandatory when on the verified path, per DENSITY) - log each variant: result.fold( onSuccess = { println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: result=Success, data=$it") }, onFailure = { println("PROTOLOG - [${Thread.currentThread().name}] ClassName.fn: result=Failure, error=$it") } )
== MANDATORY FORMAT PER FILE TYPE ==
Detect the language by file extension and use EXACTLY:
.kt / .kts -> println("PROTOLOG - <message>")
.java -> System.out.println("PROTOLOG - <message>")
.swift -> print("PROTOLOG - <message>")
.ts/.tsx/.js -> console.log("PROTOLOG - <message>")
.py -> print("PROTOLOG - <message>")
.go -> fmt.Println("PROTOLOG - <message>")
.rs -> println!("PROTOLOG - <message>");
.rb -> puts "PROTOLOG - <message>"
The message MUST start with the literal "PROTOLOG - " (with trailing space).
Standard message format: [thread] ClassName.functionName: description key=value key2=value2
One println per call site. Values go inside the string via interpolation.
NEVER use the app's logging framework (Timber, log4j, structlog, winston, direct Logcat, etc.). Use ONLY the language's basic stdout.
== SINGLE-PASS COMPILE VERIFICATION (exactly 1 compile, no retries) ==
After injecting ALL logs across ALL files, run EXACTLY ONE compile/typecheck pass that batches every touched module/target into a SINGLE invocation, so the JVM/daemon cold-start cost is paid once this round, never once per file or per fix. Do NOT attempt to fix and recompile if it fails; see below. Detect the project type and use the narrowest batched check available:
Gradle (build.gradle[.kts] present):
derive the module per touched file (replace / with : from the repo root
up to the directory containing build.gradle, prefixed with :; e.g.
feature/home/data/... -> :feature:home:data), deduplicate the module list,
and compile ALL of them in ONE gradle invocation (never one command per
module, never one command per file):
./gradlew :modA:compileDebugKotlin :modB:compileDebugKotlin 2>&1 | tail -80
Never pass --no-daemon; the Gradle daemon must persist so only the first
compile of the whole session pays Kotlin daemon/dependency-resolution
cold-start, not each round.
(if JAVA_HOME is unset and Android Studio exists, export
JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home")
Xcode / SwiftPM: xcodebuild build (scheme of the touched target) or swift build
TypeScript: tsc --noEmit (or the repo's typecheck script)
Python: python -m py_compile <touched files>
Go / Rust: go build ./... | cargo check
Anything else: the repo's documented build/typecheck command
Common PROTOLOG mistakes to avoid while writing lines (there is no recompile to catch these after the fact, so get them right the first time):
- .size on a String (use .length instead)
- .isSuccessful / .code() / .body() on non-Retrofit types (use .toString())
- ?. on a non-nullable receiver
- String interpolation calling a method not available on that type
If the pass fails, do NOT fix and recompile. Read every error in the output
(file + line + message) and return the JSON with them listed under a top-level
"compile_errors" key instead. The orchestrator surfaces them to the user, who
decides whether to fix manually or ask for a new round, rather than the agent
spending another compile trying to self-correct.
Return the JSON immediately after this single pass, whether it came back green
or with errors captured in compile_errors.
Return ONLY this JSON (do not write any summary file):
{
"files_touched": [{"path": "...", "reason": "<one line>", "log_count": <int>}],
"files_read": <int: total source files read>,
"total_logs_injected": <int>,
"self_check": "clean, or the list of what Check A/B found and you corrected",
"slice_coverage": {
"<change-id>": {
"entry": "<file:function where the flow starts>",
"boundary": "<file:function at the external boundary, or null>",
"observable_result": "<file:function where the visible result is produced>",
"hops_covered": <int>,
"gaps": [{"hop": "file:function", "reason": "technically impossible | hop budget", "detail": "<why>"}]
}
},
"compile_errors": ["<file:line: message, only present if red after the single pass>"]
}
Omit `compile_errors` entirely (not an empty array) when the pass came back green.
### Step 4.5 - Integrity backstop before commit
The agent already ran Check A and Check B on itself (SELF-CHECK BEFORE RETURNING, in
the Step 4 prompt) before it ever compiled. This step is the orchestrator's own,
independent pass of the same two checks against the agent's committed diff, run
exactly once, so the commit gate does not rely solely on the agent's self-report.
It is a backstop, not a place to fix things: unlike the pre-7.6.0 version of this
step, it does NOT edit files, does NOT loop, and does NOT run before the agent's
compile (the agent already compiled inside Step 4; running this "before compiling"
was never actually possible and is why this step is now titled a backstop instead).
The orchestrator runs, for every file in `files_touched` (skip any file present in
`/tmp/protolog-workdir-<branch>.patch`: it had uncommitted changes before the
injection, so the agent's additions cannot be cleanly distinguished from the
pre-existing ones):
```bash
python3 "${CLAUDE_PLUGIN_ROOT}/hooks/protolog-restore.py" check "<file>" <(git show "HEAD:<file>" 2>/dev/null || true)Exit 1 (Check A) means non-PROTOLOG lines were added or removed compared to HEAD (a
forbidden refactor: expression functions converted to block functions, an
intermediate variable created only to print its value, an empty else {} added,
etc). Exit 2 (Check B) means a PROTOLOG line got glued to real code (e.g. X ->
Y.also { println("PROTOLOG - ...") } inline, or ).also { println(...) } glued
to a closing paren, or anything else that is not exactly a standalone call). This
is the exact failure mode that broke a when block and two constructor calls in a
real incident; a naive line-added grep alone missed it because the offending line
does contain PROTOLOG - .
Any file exits non-zero, or the agent's JSON carries compile_errors: STOP. Do
NOT commit, do NOT fix, do NOT recompile. Narrate the offending lines (side by side
with the agent's own self_check field: a self_check: "clean" alongside a
backstop failure here is a broken contract worth calling out on its own, not just
the failure itself). The dev decides: fix by hand with the Edit tool, ask for a new
round, or restore from /tmp/protolog-workdir-<branch>.patch (Step 3).
Step 4.6 - Commit this round as [TEMP]
Once both Step 4.5 checks pass and the agent's JSON has no compile_errors key,
commit the round:
git add -A && git commit --no-verify -m "[TEMP] PROTOLOG debug logs (round <N>). DO NOT MERGE"<N> starts at 1 and increments each time inject mode runs again on the same branch
without an intervening cleanup (the dev asking for more logs after reviewing the first
batch is the common case: "${CLAUDE_PLUGIN_ROOT}/lib/helpers/git-checks.sh" temp-rounds PROTOLOG
tells you the current count; exit 2 means git failed, so stop and report it instead of reading a missing count as zero). This is what makes cleanup a plain revert instead of a text-matching
sed pass, and what keeps a fix the dev asks for mid-verification cleanly separated from
the logs: a [TEMP] commit contains ONLY PROTOLOG lines, never a fix. If the dev asks
for a fix while logs are live, that fix is committed separately (its own message,
no [TEMP] tag, never mixed into a logging commit) before or after the next round of
logs, whichever order the dev requests it in.
Step 5 - Narrate result
After the agent returns:
- Show coverage summary:
total_logs_injectedlogs across N files. - If
<ENTRY_POINTS>were given in Step 2.5, echo them back next to each slice'sentry: "Entry point you gave:<ENTRY_POINTS>. Agent anchored at:<slice_coverage.entry>." A mismatch is not necessarily wrong (the agent may have found the exact function inside the file/class you named), but flag it if the two look unrelated. - If there are
gapsin any coverage, split them byreasonand show BOTH groups:technically impossible: "Gaps (uninstrumented hops): [gap]. That part of the slice will be blind."hop budget: "Cut by the hop budget: [hop] at [file:function]. Not blind by necessity, just not covered by default." For these, ask viaAskUserQuestion: "I cut N hops here due to the hop budget. Want me to instrument any of them too?" If the dev picks one or more, invokewk:protologsagain (inject mode) with those hops passed as the entry point, same as anyNEED_MORE_DATAround: it lands as[TEMP]commit round N+1 via Step 4.6, on top of the current round.
- List
files_touchedwithreasonandlog_countper file. - Show filter commands:
(Fallback: Android Studio Logcat with filter# Basic adb logcat | grep "PROTOLOG - " # With timestamps and level adb logcat -v time | grep "PROTOLOG - " # Only System.out (excludes other tags) adb logcat System.out:D *:S | grep "PROTOLOG - "PROTOLOG -) - Remind at the end:
"When you are done testing, run
/wk:protologs cleanupto remove everything without a trace."
Mode: cleanup (/wk:protologs cleanup)
Activated when the user types /wk:protologs cleanup, or when the invoking skill reaches its cleanup step.
Before Step 1, run "${CLAUDE_PLUGIN_ROOT}/lib/helpers/session.sh" start protologs (same idempotent call as inject; covers the case where cleanup runs in a separate invocation from inject, e.g. a fresh session).
The primary path is reverting the [TEMP] commits made during inject (Step 4.6). This
is exact by construction: a [TEMP] commit contains only PROTOLOG lines, so reverting
it removes exactly those lines and nothing else, regardless of how many rounds of logs
or interleaved fixes happened in between. The sed text-match pass is only a fallback
for sessions where no [TEMP] commit exists (interrupted inject, pre-7.1.0 session).
Step 1+2 - Enumerate and revert [TEMP] commits
"${CLAUDE_PLUGIN_ROOT}/lib/helpers/git-ops.sh" revert-temp PROTOLOGEnumerates every [TEMP] PROTOLOG commit and reverts each one, most recent first, always
through an auto-abort wrapper so a conflict never leaves the repo sitting in a
half-resolved revert. Fixes the dev requested mid-verification live in separate,
non-[TEMP] commits and are untouched by these reverts. Output, one line per commit:
NONE(nothing found): narrate "No [TEMP] PROTOLOG commits found; falling back to text-match cleanup." and go to Step 2-fallback.REVERTED <sha>: that commit's logs are gone, nothing further to do for it.CONFLICT <sha> <files...>: that commit's revert conflicted (rare: a fix touched the same lines as a log) and was auto-aborted; run Step 2-fallback for exactly those files. Step 3b is mandatory for them, exactly as for any other fallback-cleaned file.
Exit 0: every commit reverted cleanly, skip to Step 3. Exit 1: at least one CONFLICT
line, handle each via Step 2-fallback below, then continue to Step 3. Exit 2: STOP
immediately, narrate the exact error, do not proceed. It means a revert was ALREADY in
progress before the script started (the dev's own: never abort it, ask the dev), or
REVERT_HEAD stayed set after the script's own abort, or git failed. Exit 2 means the base is unusable or git failed: stop and report it, never read it as an empty or clean result.
ON CONFLICT (mandatory, no exceptions):
FORBIDDEN:
- Resolving the conflict by hand (editing the conflicted file,
git addon it). - Running
git revert --continueorgit revert --quit. - Running
git commitwhile a revert is still in progress. BAD: fix the conflict markers with the Edit tool,git add,git revert --continueGOOD: take theCONFLICTline's file list, run Step 2-fallback for those files only
git-ops.sh revert-temp already confirmed the abort took (git rev-parse -q --verify
REVERT_HEAD printed nothing) before reporting CONFLICT, and already kept reverting the
remaining [TEMP] commits. The only manual step left is Step 2-fallback, for the files
named on the CONFLICT line.
Why this is absolute even when the conflict looks trivial to resolve: cleanup's guarantee
rests on exactly two verified mechanisms, a revert is exact by construction (a [TEMP]
commit contains only PROTOLOG lines), or the sed fallback carries its own checks (embedded-
line grep, Step 3b phantom reconciliation, Step 5 compile gate). A hand-resolved revert is
a third, unverified mechanism: its content is whatever was typed, yet Step 3b then skips
reconciliation because it assumes the revert path was exact. In a real incident an agent
hand-resolved a conflicting revert and ran git revert --continue; the end state happened
to be correct, but nothing in the flow verified it. Any command that completes the revert
with hand-chosen content is the same violation, whatever it is spelled as. A PreToolUse
hook (hooks/protolog-revert-conflict-guard.sh) also denies --continue/--quit/git
commit while REVERT_HEAD points at a [TEMP] PROTOLOG commit, as a backstop.
Collapsing each [TEMP]/revert pair out of history is left to the wrapup squash (doer) or
to the dev directly (standalone); cleanup's job is just to remove the logs, not to rewrite
history.
Step 2-fallback - Text-match removal (only when no [TEMP] commit applies)
"${CLAUDE_PLUGIN_ROOT}/lib/helpers/git-checks.sh" trace PROTOLOGRun it as its own command, never piped: a pipe hides the exit code. Exit 2 means the base is unusable or git failed: stop and report it, never read it as an empty or clean result.
Tracked and untracked files alike. On exit 1 the files are the part of each printed line
before the first :, deduplicated. On exit 0 (nothing printed) narrate:
"No lines with PROTOLOG - found in the repo. The code is already clean."
and stop.
For each found file:
python3 "${CLAUDE_PLUGIN_ROOT}/hooks/protolog-restore.py" strip "<file>" > "<file>.protolog-restored" \
&& mv "<file>.protolog-restored" "<file>"This is the SAME transformation the integrity guard checks before a [TEMP] commit
lands (see "SELF-CHECK BEFORE RETURNING" and Step 4.5), so it is exact by
construction, and strict by default: a nonzero exit (2, an embedded or malformed
line, e.g. .also { println("PROTOLOG - ...") } glued to real code, or business
logic after the call's own close) means STOP and fix that file with the Edit tool
by hand rather than force a partial strip; do not guess with sed.
Only when the strict pass refuses AND the offending line is confirmed to be exactly
the documented embedded shape (.also { println("PROTOLOG - ...") } glued onto
real code; should not happen under the inject rules, but a manual edit or an older
session can still produce one) does the lenient fallback apply:
python3 "${CLAUDE_PLUGIN_ROOT}/hooks/protolog-restore.py" strip "<file>" --lenient > "<file>.protolog-restored" \
&& mv "<file>.protolog-restored" "<file>"which strips just the .also { println(...) } suffix, keeping the rest of the
line, and reports which line numbers it touched to stderr. Re-run the strict
strip (no --lenient) on that file afterward to confirm no PROTOLOG trace and no
broken syntax remains (exit 0, no output changes); if it still does not match
cleanly, stop and fix that file with the Edit tool by hand instead of guessing
further with the lenient pass.
Narrate the processed files.
Step 3 - Verify complete cleanup
Mandatory regardless of which path (revert or fallback) was used. This is the check that was skipped in the real incident: cleanup was reported as done without confirming the result actually compiles.
Before anything else, confirm no revert is still in progress. A live revert here means Step 1+2's ON CONFLICT rule was not followed:
"${CLAUDE_PLUGIN_ROOT}/lib/helpers/git-checks.sh" revert-in-progressIf it exits 1 (prints something), STOP: do not verify, do not report cleanup as done.
Narrate that a revert is in flight, run git revert --abort, and return to Step 1+2's
ON CONFLICT block. If it exits 2, git failed: stop and report it, never read it as "no revert in progress".
"${CLAUDE_PLUGIN_ROOT}/lib/helpers/git-checks.sh" trace PROTOLOG- Exit 0 (empty) -> narrate: "No trace of
PROTOLOG -remains. Proceeding to restore phantom files." - Exit 1 (lines printed) -> narrate which ones, do NOT delete them automatically; ask the user to review them manually before continuing.
- Exit 2 -> git failed: stop and report it, never read it as an empty or clean result.
Step 3b - Restore files outside the original diff
This step is mandatory when the fallback (Step 2-fallback) ran. When the revert
path (Step 1+2) ran, the reverted [TEMP] commit already restored exactly the files it
touched, so there is nothing left to reconcile; skip straight to Step 4. Only the sed
fallback needs this reconciliation, because the logger-agent intentionally instruments
files outside the branch diff (callers, use cases, repositories) and sed has no
concept of "this file's changes came from one commit".
Determine <BASE>: run the same auto-detection as inject Step 1 (upstream
tracking -> develop -> main -> master). If multiple candidates are found, show them
to the user via AskUserQuestion and let them pick one (same helper as inject Step 1:
"${CLAUDE_PLUGIN_ROOT}/lib/helpers/git-checks.sh" base-candidates). The chosen branch
is <BASE> for the rest of this step.
Cross-reference which dirty files are outside the branch's intended changes (phantoms
-- clean before the logger agent touched them), and restore the ones that are ALSO
byte-clean now that sed (Step 2) already removed their PROTOLOG lines. Restore is
conditional: the logs go, but any correction the user made to a phantom during
testing must be kept.
"${CLAUDE_PLUGIN_ROOT}/lib/helpers/git-ops.sh" restore-phantoms <BASE>For each phantom, this runs git diff --quiet -- "<phantom>" and either restores it
silently (exit 0, no diff vs HEAD: the agent only added PROTOLOG lines and they are now
gone, git restore clears any residue) or prints its path without touching it (diff
exists: non-PROTOLOG changes, may be corrections the user made or a surviving refactor).
For each printed path, show git diff -- <path> and ask the user: "This file has
changes after cleanup. Keep or discard?" Only run git restore <path> if they choose to
discard.
The script classifies every phantom first and restores the clean ones in a single call, so an error never leaves a half-done restore. Exit 2 means the base is unusable or git failed: stop and report it, never read it as "no phantoms" (nothing was restored).
Narrate each file restored and each file kept per the user's choice. Never force-restore a phantom with non-PROTOLOG changes without the user's explicit confirmation.
Step 4 - Final working-tree confirmation
Run a last sanity check and narrate the result:
git status --short- If the only modified files are those from
git diff <BASE>...HEAD --name-only(the branch's intended changes, minus their PROTOLOG lines) -> narrate: "Working tree is clean of PROTOLOG. Only your intended branch changes remain." - If unexpected files still appear -> show them and ask the user to decide.
Step 5 - Mandatory compile verification before reporting success
Never narrate the working tree as clean without running this step. Reporting
cleanup success without validating the result is exactly what let the earlier incident
(corrupted when block, unclosed constructor calls) go unnoticed until the next build.
Detect the affected modules from the files touched by the reverted [TEMP] commits (or,
on the fallback path, from files_touched in the original inject JSON), deduplicate them,
and run the same batched, single-pass check as inject's compile verification (see
Step 4), one gradle invocation covering every affected module, e.g.:
./gradlew :modA:compileDebugKotlin :modB:compileDebugKotlin --stacktrace 2>&1 | tail -60- Compiles clean -> narrate "Working tree is clean of PROTOLOG and compiles clean."
- Fails -> narrate the exact error and STOP. Do NOT report cleanup as done and do NOT recompile automatically (fallback path: this likely means an embedded PROTOLOG line the first pass missed; revert path: this should not happen since Step 4.6 already required a clean compile before committing, so treat it as noteworthy). Hand the error off to the user to decide the fix rather than looping recompiles -- this mirrors inject's single-pass cap.
Step 6 - Release the session marker (standalone only)
If this cleanup run was invoked directly (/wk:protologs cleanup typed by the user),
run "${CLAUDE_PLUGIN_ROOT}/lib/helpers/session.sh" stop. If it was invoked by /wk:doer
Stage 4 or /wk:bugfix Stage 6, skip this: those skills own the marker's lifecycle and
release it themselves at their own wrapup, since the parent session is still active after
this cleanup step returns.
General narration rules
- Begin each action with a short English line describing what is being done.
- Never use em dashes in any response.
- Build or git errors are always narrated; never silenced.
- This skill's only commits are its own
[TEMP]PROTOLOG commits (inject) and their reverts (cleanup). It never commits or pushes business logic, and it never pushes anything to a remote. Any real fix the dev asks for mid-verification is committed separately by whichever workflow owns that change (doer's Stage 3/4 loop, or the dev directly when the skill runs standalone).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

