Imported from innovation-upstream/devrc (
claude/skills/check-clickup-addressed/SKILL.md). Install upstream withnpx skills add innovation-upstream/devrc --skill check-clickup-addressed. Copyright stays with the author.
/check-clickup-addressed ā verify if ClickUp tasks were fully addressed
Deterministic check: given the N most recent comments not from you on your assigned tasks, find the sessions that worked on those tasks, read the transcripts, and report what's done vs what's still open.
Run the cheap pass first ā clickup's awaiting
š“ If the question is which tasks are waiting on you, this is the wrong tool. The
clickup skill answers that on its own, with no transcript reads:
node query.mjs awaiting # from the clickup skill dir; --max bounds the fan-out
awaiting decides on ONE predicate ā the newest comment on the task was not authored by
the token owner ā and sweeps the whole assigned queue in ~45s at one API request per
task. It cannot tell you whether anything got done; that is the question this skill
exists for, and answering it costs a transcript read per task.
So: awaiting to get the list, this skill on the few entries that matter. The two are
complementary, not alternatives, and the split is by QUESTION (which vs whether), not by
speed. Running both does duplicate one ClickUp fan-out ā step 1 below makes its own pass ā
which is cheap for a handful of tasks and wasteful across the whole queue.
Quick start
The scripts live in devrc, not in this deployed skill dir ā ~/.claude/skills/ is a
read-only nix-store copy of claude/skills/, and only the docs ship there. Run them from
the repo:
š“ Transcript scanning is OPT-IN since 2026-08-22 (--transcripts). Measured upstream
over the three tickets in that day's report, the four evidence sources scored: ClickUp
status 3/3 useful; newest comment 3/3 and decisive in every case; transcript scan
0/3, one of them actively misleading (a ā PR merged whose own snippet said the bug was
still unfixed); cited-PR resolution 0/3 resolved, 2/3 with FALSE explanations. The scan
is also ~60s of the ~90s runtime and the origin of every false verdict this tool has
shipped. The ClickUp-side default runs in ~21s and produced only correct output on the
same input.
CCUA=~/workspace/devrc/scripts/check-clickup-addressed
# Default: top 3 comments, ClickUp status + newest comment (~21s). NOT the FOUR
# transcript-dependent flags ā the run names every one of them and says it skipped
# them. `--since`/`--include-self-runs`/`--no-resolve-prs`/`--verbose` are inert
# here and warn on stderr rather than being silently ignored.
python3 "$CCUA/check-addressed.py"
# Add the transcript scan and completion verdicts (~90s; read every snippet)
python3 "$CCUA/check-addressed.py" --transcripts
# Fast mode: samples only the 10 most recently updated tasks (~10s)
python3 "$CCUA/check-addressed.py" --fast
# Top 5, JSON output
python3 "$CCUA/check-addressed.py" --limit 5 --json
# Only sessions since a date
python3 "$CCUA/check-addressed.py" --since 2026-08-15
# Verbose; skip the gh PR lookups
python3 "$CCUA/check-addressed.py" --verbose --no-resolve-prs
š“ Editing this file needs a home-manager switch; editing the scripts does not.
claude/skills/check-clickup-addressed/SKILL.md + reference/ deploy through nix, so a
prose change is invisible to a session until the switch. scripts/check-clickup-addressed/
is run straight out of the working tree ā the edit is live immediately, and it is gated by
scripts/run-tests.sh (target scripts/check-clickup-addressed/tests).
What it does (deterministic pipeline)
recent-comments.pyā queries ClickUp for all comments on your assigned tasks, filters out your own, sorts by date, returns the top N with task ID/name, ticket status/priority, author, snippet + fulltext, andmy_latest_replyā the date of your newest comment on that task, computed before your comments are dropped, because the consumer otherwise cannot tell an answered ticket from an abandoned one. That last key is omitted entirely when the user id could not be resolved, or when a date on one of your comments would not parse ā absent means "the check could not run", which is not the same fact as a presentnull("I looked; you never replied"). Both consumers branch on which. Beside the two display dates it also emits the raw epoch-ms they were formatted from (date_ms,my_latest_reply_ms), becauseformat_datethrows the seconds away and the "have I answered?" comparison then runs at minute resolution. Each is emitted only when it parses ā absent, never0, andmy_latest_reply_msis nested undermy_latest_replyso a precise instant can never outlive the reply it refines.search-sessions.pyā searches~/.claude/projects/**/*.jsonltranscripts for sessions mentioning a task ID, ranks by hit count. Terms are ANDed, so it is run once per task, never with several tasks' terms in one query.check-completion.pyā for each task, reads the matched sessions' assistant text and pattern-matches for completion signals (merged/shipped/verified) vs open signals (still open/not yet done/needs action), within ±2000 chars of a mention of that task's ID, and only where that task is the nearest ID to the match.check-addressed.pyā orchestrates the above, produces a unified report.
Both search and completion skip the current session ($CLAUDE_CODE_SESSION_ID),
which necessarily mentions every task ID under test. Override with --exclude-session ID
(repeatable) or, on check-completion.py, --include-self.
š“ They also skip every PRIOR RUN of this checker ā recognised structurally by
scripts/check-clickup-addressed/_selfrun.py, not by session id. A previous
report prints each task ID directly beside likely_addressed / ā resolved / merged,
which is precisely the shape the proximity scorer rewards, so without this the tool reads
yesterday's verdict back as today's evidence and re-confirms it forever. The report prints
(ignored N transcript(s) ā¦) when the guard fires; --include-self-runs disables it.
Individual scripts
š“ THIS SKILL DIRECTORY IS NO LONGER SELF-CONTAINED (2026-08-24). search-sessions.py
and check-completion.py import scripts/lib/transcript_search.py ā the ONE transcript
walk they now share with scripts/find-session.py ā resolved as _HERE.parent / "lib".
Consequences worth knowing before you move, copy or deploy this directory:
- copying the skill dir alone gives you scripts that cannot import; the sibling
lib/has to come with it.tests/mutation_sweep.py::materializereproduces the repo'sscripts/layout for exactly this reason, and itsSHARED_MODULESlist is pinned two-way bytests/test_shared_walk.py. - a change to that module changes THIS skill's verdicts. Its own regression ledger lives
in
scripts/tests/test_transcript_search.py; run both suites, not just this one. - the two search axes this skill does NOT take from the library default ā
surface=SURFACE_TOOL_USEandinclude_sidechains=Trueā are passed explicitly insearch-sessions.pyand commented there.
All scripts accept --json for machine-readable output. Two of them make network calls:
recent-comments.py needs ClickUp credentials, and PR resolution shells out to gh pr view ā on by default in check-addressed.py (--no-resolve-prs opts out), off by
default in check-completion.py (--resolve-prs opts in). Only search-sessions.py is
unconditionally hermetic. The PR cache is per-process and the orchestrator forks one
process per task, so lookups dedupe within a task, not across the run.
| Script | Input | Output |
|---|---|---|
recent-comments.py --limit N |
ClickUp API | --json: task_id, task_name, task_status, task_priority, date, author, snippet, text, and my_latest_reply only when the user id resolved AND every comment of yours could be ranked ā its ABSENCE is meaningful (the check could not run), so read with in, never .get(). Plus date_ms / my_latest_reply_ms, the raw epoch-ms behind the two display dates, each present only when it parsed. The tabular printer emits a different, smaller set ā task_id, task_name, date, author, snippet ā which check-completion.py parses for field 0 only. |
search-sessions.py term1 [term2 ...] |
~/.claude/projects/ |
session_id, date, project, hits |
check-completion.py --task ID |
session transcripts | status, completion signals, open items |
check-addressed.py --limit N |
all of the above | unified report |
Interpreting results
| Status | Meaning |
|---|---|
likely_addressed |
Completion signals found, no open signals |
partially_addressed |
Both completion and open signals ā something shipped, something remains |
open |
Only open signals found ā work not completed |
unclear |
Sessions mention the task, but no signal of either kind is attributable to it |
no_sessions_found |
No session transcript mentions this task at all |
no_mentions_found |
Sessions were read, but the task ID appears in none of them ā no evidence either way, never an implied "partly done" |
not_scanned |
The DEFAULT since 2026-08-22. No transcript was read at all, so no verdict was formed. Distinct from every row above: those are searched outcomes, this is the absence of a search. The record also omits mentions_found and sessions_searched rather than reporting 0 ā an unsearched zero is not a searched one. --transcripts is what produces the other rows |
š“ partially_addressed is the status to be most suspicious of ā it was, until
2026-08-19, the state an unmatched task fell into by construction (see Limitations).
Check mentions_found > 0 before believing any verdict, and read the snippets: a signal
is attributed by lexical proximity, so it can be a real sentence about the right task
that still says nothing about whether the ticket is done.
Completion signals detected
- PR merged/landed/shipped (references like
#1234 merged) - Verified on main/trunk
- Fix shipped/deployed
- Resolution markers
Open signals detected
- Still open/pending/waiting
- Left open (deliberately)
- Not yet done/fixed/deployed
- Premise refuted (scope changed)
- Needs action/decision
- Deployed ā verified
Limitations
š“ This tool reports which words appeared near a task ID. It does not know whether the
work is done. Treat every verdict as a pointer to a transcript to read, never as an
answer. Three structural false-positive generators were fixed on 2026-08-19, a fourth ā
the checker reading its own previous runs ā on 2026-08-20, and on 2026-08-21 two silent
no-ops: an unrecognised ClickUp status disabling every ticket/comment cross-check, and an
inert proximity tier whose confidence marker printed the same symbol for every signal.
Round 5, the same day, fixed a false explanation (a named-but-unknown repo reported as
"repo not named"), the repaired marker never reaching the report, and the missing
"nobody is on it and someone is waiting" flag (matrix in
claude/skills/check-clickup-addressed/reference/validation-history.md). Round 6, on 2026-08-22, fixed
that new flag's own blind spot ā it could not see your replies, so a ticket you had
already answered was reported unanswered forever. What remains is listed under "Still weak".
š“ A fix for the self-run guard was TRIED and REVERTED (2026-08-22) ā recorded so nobody
re-derives it. The guard drops any transcript containing this skill's markers, which
measured as 67% / 80% / 100% of matching transcripts on three live tickets. That looks
like catastrophic evidence loss, so the guard was rewritten to redact the checker's output
messages and keep the session. It recovered the transcripts (3ā8, 2ā9, 0ā8) and made
every verdict worse: all three tickets went to partially_addressed on completion signals
mined from sessions that were working on this checker. One such snippet was literally a
test-corpus example ā a sentence of the form "the fix landed in #NNNN and is live" ā scored
as completion evidence for an unrelated infrastructure ticket. Every recovered session for
one of the three opened with either /check-clickup-addressed or "read and evaluate the
check-clickup-addressed skill". The crude guard was reaching the right outcome by the
wrong mechanism, and the ~81% it discards is overwhelmingly noise. The real lesson is the
one that made the scan opt-in: lexical proximity cannot separate a session that WORKED the
ticket from a session that TALKED ABOUT it, and no amount of guard-tuning changes that.
(Ticket ids and the client's stack are omitted here ā this repo is public. The measurement
is what carries; the identifiers were the client's.)
š“ Every round so far has been found by RUNNING the tool and checking its verdict against reality, never by reading the code. Round 6's defect shipped with a fresh adversarial test suite that had five controls on the very function that was wrong ā all five green, all five scoped to one side of a seam. If you are about to trust a verdict here, open the ticket.
- Session search is keyword-based, not semantic ā a session that addressed the task
without ever writing its ID is invisible to this tool. A
no_mentions_foundorunclearverdict is not evidence that nothing was done. - Completion detection is pattern-based ā it catches common phrases ("merged", "shipped", "verified on main") but may miss unusual phrasings.
- The pipeline runs sequentially (ClickUp API ā transcript search ā pattern match), so it's bounded by API latency for the comment fetch (~30s for 50 tasks).
Still weak (measured 2026-08-19, not fixed)
- A signal is attributed by lexical proximity, not by meaning. Nearest-ID keeps a
neighbouring task's verdict out, but not a neighbouring clause's:
#1065 ⦠merged 08-16 (I confirmed). `868gx0aaa` is u[nfixed]still scores as completion for868gx0aaa, because the merge sentence really is adjacent to it. Read the snippet. - A PR reference the tool cannot pin to a repo is reported
unresolved, not guessed ā a snippet-wide scan was tried and attributed every bare#Ntocivitai/civitai, returning a real but unrelated PR ("merged 2024-03-18"). š“ Round 5 split the failure into three precise messages; round 7 (2026-08-22) COLLAPSED two of them back, on measurement.repo 'X' is not in KNOWN_REPOSasserts that a repo was named and is spelledXā and on a live upstream run 2 of the 3 cited PRs rendered asrepo 'their' ā¦andrepo 'which' ā¦, the lookbehind having captured the preceding English word while the comment plainly named a repo. The precise message was wrong more often than the vague one it replaced, and it sends the reader to add an English word to a repo table.wordis simply whatever token precedes the#, and nothing short of enumerating the world separatesdevrcfromlandedā so the tool no longer claims to know. Both now renderunresolved (could not determine the repo; if one is named here, add it to KNOWN_REPOSā¦)ā a conditional, true either way, which keeps the affordance without the false premise.ambiguous ā A, B both in rangesurvives: repos genuinely were named there, so naming them is a true diagnosis.KNOWN_REPOSis a closed vocabulary, so widening it is not the fix (same shape as the status sets below); verify owners withgh repo viewādevrcis innovation-upstream/devrc, andcivitai/devrcdoes not exist. --fastonly inspects the 10 most recently updated tasks, so a stale-but-important task is invisible to it. That is a sampling window, not a full check.
What the report now cross-checks for you
Three independent sources, and the Needs a decision block fires on their disagreements ā plus one state where nothing disagrees and that is exactly the problem (4):
- ClickUp status + priority, printed beside every verdict.
- The ticket's newest comment ā usually the real record of what happened. A comment
reading "Resolved / recommend closing" over a ticket still at
to dois flagged; that exact case (868gx0aaa,to do/urgent) is why the check exists. š“ An explicit refusal to close VETOES that flag: on 2026-08-20 an alert-cycling clause (a service whose queue-depth alert fired and resolved repeatedly) tripped the keyword on868gx0bbb, whose opening clause refused closure outright ā the report told the operator to close it. The reporter's own words outrank both the keyword scan and the transcripts. š“ The veto has TWO TIERS ā an absolute one was wrong in both directions (round 8; 17-case before/after table in the validation doc). STRONG ("do not close", "still live", "staying/remains open", "reopening") is about this ticket and is absolute. WEAK ("still open", plus any negated closure claim) is as often about a PR, an alert or a sibling ticket: it vetoes when it is the comment's only word on closure, and drops to "READ IT and decide" when another clause carries an un-negated closure claim. Untiered, "Resolved⦠recommend closing. (The follow-up PR is still open but unrelated.)" emitted do NOT close. š“ Negation is decided ONCE, scoped to the CLAUSE and to word order (closure_claims) ā never per-word lookbehinds, which guard only the spellings they enumerate and lost this twice on the abandoned branch. Untreated,isn't/never/won't/not fully/unresolved/cannot/far fromand "I do not recommend closing" all drew an affirmative close it over a comment refusing exactly that. Contractions match by shape (\w+n't): any stem list is already missingwon'tthe day it is written. š“ Commas ARE clause boundaries; parens are not; a negator negates only what FOLLOWS it. Each of those was wrong at least once, each wrong version shipped an affirmative wrong instruction, and two blind audits found them. A trailing "no" reaching backwards turned "Resolved, no further action needed" into do NOT close (12 of 12); a symptom's negator reaching forward across a comma turned "The alert wasn't firing, resolved by the rule fix" into do NOT close (10 of 10); parens stranding a negator turned "This is not (yet) resolved" into close it. š“ DO NOT ARGUE THE NEXT CHANGE FROM AN EXAMPLE ā SCORE IT.scripts/check-clickup-addressed/tests/test_corpus.pyholds 49 labelled comments, every one from a measurement, and 8 recorded KNOWN FAILURES. Trunk scores 24/42 on the original set, what shipped 38/42. Add your motivating case with the verdict a human would give, then change code. š“ AND AUDIT THE CORPUS ITSELF ā it was blind to its own worst class. All 18 negator/closure pairs in the first 42 put the negator FIRST, so a negator that FOLLOWS the word it denies ("The ticket says resolved but it isn't" -> close it, on trunk and now) appeared nowhere and the close-it guard could not fail on it. A mutant narrowing the negation window to 30 chars kept the suite green AND the score unchanged while flipping five refusals to "close it", because the corpus's cases all clustered under 27 chars. Ask which shapes your instrument structurally cannot see, and who wrote the labels. - Cited PRs, resolved against GitHub (
gh pr view) rather than believed on sight. A completion signal quoting a PR that is actually still open is flagged ā on 2026-08-19 that shape was real (talos-infra #1073). Disable with--no-resolve-prs. - Nobody is on it and someone is waiting ā a not-done ticket + zero transcript
evidence (
mentions_found == 0) + a comment from someone else withinUNANSWERED_COMMENT_DAYS(14). No rule produced this before 2026-08-21 because nothing disagrees: ticket open, transcripts empty, and they agree. Measured live on868gz0hhh(to do/high, colleague comment, 0 mentions) the block said nothing. š“ Bounded by recency, not priority ā priority is a stale property of the ticket and would re-fire on every unstarted backlog item forever, training the reader to skip the block; recency is a property of the interaction, says a human is waiting now, and self-clears. Fires whatever the status word reads (safe direction, like the veto). š“ It could not see YOUR OWN replies until 2026-08-22, so an answered ticket stayed flagged forever ā the unbounded noise the recency bound exists to prevent. Measured on868gz0hhh: "Commented 2d ago; nobody has answered" over a ticket two sessions had already answered, the later of them 11 h earlier, and acting on it duplicated an analysis already in the thread. A seam defect, each side correct alone ārecent-comments.pydrops your comments (right: the report is about what others said), and the flag concludes nobody answered (right, given the only evidence it is handed). Neither can see the other's assumption, so the fix crosses the seam: the producer now emitsmy_latest_replyper task and the flag suppresses when your newest reply is at or after the comment. Compared, not counted ā a reply predating the question does not answer it. An absentmy_latest_reply(stale producer, a failed user-id lookup, or a date on one of YOUR comments the producer could not read) fires but says the check never ran, rather than silently deciding either way. š“ Compared on the raw epoch-ms, not on the displayed minute (2026-08-22, round 7).format_datethrew the seconds away before anything compared them, so a reply written 20 seconds BEFORE the question rendered identical to it and counted as an answer ā a waiting colleague dropped from the report. Whether a tie means "answered" was a judgement call argued to opposite conclusions from the same evidence; the producer HAS the raw ms, so it is now a question of fact and a tie means the same millisecond.date_ms/my_latest_reply_msride beside the display fields and are used only when BOTH are present ā comparing one raw instant against a rounded one is worse than comparing two rounded ones ā otherwise the minute-age comparison is used unchanged. An unreadable date yields an absent ms, never a0: zero is 1970, which would make every reply look newer than every question. š“ A SUPPRESSED flag now prints one line of its own, and that is where the bot-identity caveat lives. ClickUp has no bot identity: every comment posted through thepk_token comes back authored as the token's owner whoever typed it, so "you answered" and "an agent answered as you" are the same observable ā an agent-posted comment setsmy_latest_replyand suppresses this flag. Suppression used to print nothing at all, so a genuinely-waiting colleague left the report with no trace ā and with the transcripts finding nothing, the flag that would have caught it was the one being suppressed. The line goes in its own block (## Answered already ā no action, but check who answered), never in Needs a decision ā a "no action" line in the act-on-this block is how a block stops being read ā and the caveat leads that block once, not per line (199 chars Ć every line was ~11 KB in a--limit 20report, in the one block whose justification is that volume kills a block). Each note keeps the caveat's consequence. š“ The note is bounded, and it must be able to PROVE it is bounded. It carries the same recency window as the flag; the bound falls back todate_mswhen the display date is unreadable (otherwise a driftedformat_datekills the bound for every record while the ms path keeps deciding ā measured: a 2019 ticket printed an unbounded note); and if NO bound can be evaluated from either field the record falls through to the flag instead, since an un-expirable "no action needed" line is exactly the permanent noise this is guarding against, while a call to action should survive an unreadable date. š“ The note states other coverage as a COMPUTED fact, never an assumed one. It used to assert that nothing else in the report named the ticket, becausementions_found == 0ā false in four reproduced shapes (unknown status / RESOLVED-reading comment / keep-open veto / open cited PR, each printing a Needs a decision line about the same ticket directly above it), and a non-sequitur besides: not one of those four rules readsmentions_found. It now runsdisagreements([r])on that ONE record and says what came back ā[r], not the whole result list, or every note in the report inherits one ticket's flag. š“ The seam is crossed for TOP-LEVEL comments only.recent-comments.pycalls the CLI without--threads, and/task/{id}/commentreturns top-level comments only ā replies live behindgetThreadedComments. So an answer you wrote inside a comment thread is still invisible and the ticket still reads as unanswered. Narrower than D12, same shape. (Verified 2026-08-22 that the two answers on868gz0hhhare top-level, so the motivating case really is fixed ā but do not read that as the general case.) š“ THIS FLAG HAS ITS OWN CORPUS ā SCORE IT, DO NOT ARGUE FROM THE EXAMPLE.tests/test_waiting_corpus.pyholds 21 labelled RECORDS (pre-port 10/21, shipped 21/21).test_corpus.pycannot help here: it scores comment TEXT and this flag reads FIELDS. Verdicts are read off the claim ā "nobody has answered" vs "the check did not run" vs "the date was unreadable" vs the suppression note vs silence ā because "the flag fired" is satisfied by four lines that tell a reader four different things. š“ New false-SILENCE direction, deliberately unbounded by transcripts: reply "I'll look next week" and the flag is silenced for that comment even though the transcripts showed no work ā which is the gap this flag was added to fill. (Do NOT restate that as "mentions_found == 0means no other rule covers the ticket": that is a non-sequitur, retracted 2026-08-22. A zero mention count says the TRANSCRIPTS are empty; the status, comment and PR rules never read it and can each still flag the ticket.) Bounded only by a newer colleague comment re-firing it. Acknowledging is not doing.
š“ FOUR of these checks NEED the transcript scan, and it is OFF by default ā the run names
every one of them. The cited-PR resolution (source 3), the waiting flag (source 4),
"ClickUp <done> but open signals remain", and "transcripts read as done while ClickUp is
still open". The first two of those are gated on a SEARCHED zero; the other two need a
completion verdict and an evidence list that a scan-less run never builds. Since the scan
became opt-in the scan-less record carries no
mentions_found at all ā deliberately, because an unsearched zero is not a searched one ā
so in the default invocation those rules are structurally unable to fire. Upstream that
was silent, and when nothing else disagreed the Needs a decision heading was not
printed at all: measured by driving the tool, an empty block reading as checked, nothing
disagrees when it meant rules never ran. Exactly the failure round 4's unknown-status
announcement exists for, one axis over, and it arrived with a change that was individually
correct. A default run now prints one line naming every such rule and how to enable them.
Pass --transcripts and the line disappears.
š“ The list is BUILT from a ledger (SCAN_ONLY_RULES), not restated in prose, because the
first version of that announcement named TWO of the four. Naming a subset is worse than
naming none ā it implies the unnamed ones ran, which is the same defect one axis further
over. The ledger pairs each rule with its flag's own unique tail, and a test diffs a
fully-armed fixture across both modes: it fails when a fifth scan-dependent rule is ADDED and
when one is REMOVED. A second test forbids a rule's announcement NAME from containing its own
TAIL, so "<tail>" in output can never be true in a run where that flag did not fire ā the
instrument-matching-its-own-announcement trap that cost upstream a false failure.
š“ The two guards deliberately key on different facts, and neither is a typo: the flags
gate on the STATE ("mentions_found" in r) because they ACT on evidence and a renamed status
sentinel must not be able to fire them; the announcement keys on the run's own declaration
(status == "not_scanned") because it DESCRIBES the run, and a record merely missing the key
(a stale producer, a partial write) is not evidence that the whole scan was skipped.
š“ Sources 1 and 2 are gated on a hardcoded nine-word status vocabulary, and ClickUp
statuses are per-list and arbitrary. Until 2026-08-21 a miss was silent: measured,
in review / blocked / needs qa / review each disabled every ticket/comment check
including the keep-open veto, and printed nothing ā an empty Needs a decision block
reads as checked, nothing disagrees when it means not checked. An unrecognised status
now announces itself there and names the two sets to add it to. Widening the sets is not
the fix on its own ā it moves the silence to the next unknown word; keep the
announcement.
Improvements over naive approach
- Windowed search: only looks for signals within ±N characters of each task ID mention, not across the entire session transcript
- Adjacency weighting (
[ā]/[ā]): a signal whose own ±40-char snippet contains the task ID ([ā]) outranks one merely somewhere in the ±2000-char window ([ā]).[ā]is the one to distrust ā "same window" can mean a different paragraph about different work. In the report it is a second, bracketed column: first glyph =ācompletion /āopen (WHAT the signal says), bracketed = adjacency (HOW MUCH to trust it). Two orthogonal facts, deliberately not merged āāalready meant "open item" here. š“ Inert until 2026-08-21, then invisible for a day: first aproximity > 0.5tier ranked signals by adistancethe only production producer hardcodes to0, so everything scored ā„ 1.0 and the marker printedāfor every signal (tier deleted; verdicts unchanged on all six live tasks); then the repaired marker was wired only intocheck-completion.py's printer, not intocheck-addressed.pyā the entry point everyone runs ā so it still reached no report. A marker nobody sees is the same no-op as one that cannot vary. Live 2026-08-21:868gy0dddshows both values,868gy0eeeis all[ā]. - Deduplication: same signal from multiple windows is only reported once
- Cross-task attribution guard: a signal is kept only if the task under test is the ClickUp ID lexically nearest to it. Without this, a triage table listing several tasks reports every task's verdict as every other task's.
- Exact task-ID matching only: a 6-character-prefix pass was removed on 2026-08-19 ā it matched neighbouring tasks sharing a prefix and never fired usefully, since ClickUp always supplies the full ID.
- Self-exclusion: the session running the check is skipped, so the tool cannot read its own output back as evidence.
- Prior-run exclusion (2026-08-20): every transcript that is a run of this checker is
skipped too, recognised by anchored markers rather than session id. The markers had to be
anchored. Measured 2026-08-21 over the 735 transcripts these scripts actually walk
(not a recursive grep, which also sees 4,795 files under
<session>/subagents/that are never opened): a barecheck-clickup-addressedmatches 67 (9.1%) because the skill catalog is injected into every session, and a bare/check-clickup-addressedmatches 24 (3.3%) because it is a substring of the skill's own path ā against 4 for the real anchored markers. An earlier "213 of ~250 / 32" came from the recursive walk and was wrong on both numerator and denominator; the verdict survived re-measurement, the figures did not. š“ These drift ā 746/62/23 one day, 735/67/24 the next. Re-measure before quoting. Failure direction is deliberate: a false positive drops a session and degrades a verdict tounclear; a false negative brings back a confident ā over a live ticket. - š“ Self-classification creep, and it now has a REAL victim. The markers appear in this
SKILL.md, so any session that loads the skill, reads this file, or reviews its diff becomes
a permanent self-run ā and a session that both did work and ran the checker is dropped
from tomorrow's evidence. Measured 2026-08-21: 5 task IDs are visible only inside
dropped transcripts, and one of them is a LIVE ticket, not a test fixture ā its only
mentions sit in a work session that also ran the checker. An earlier note claiming "real
loss is currently zero" was true when written and is no longer. The drop lands in the safe
direction (ā
no_mentions_found, i.e. no evidence, never a false ā ) but it lands on the session most likely to contain the work. Nothing ages a marker out; the set only grows.--include-self-runsis the escape hatch when you suspect this.
Performance
| Mode | Time | Notes |
|---|---|---|
--fast |
~10s | Samples only the 10 most recently updated tasks |
default (no --transcripts) |
~21s | ClickUp only; no per-task transcript sweep |
--limit 5 --transcripts |
~85ā100s | All assigned tasks; one transcript search per task |
ā ļø Every figure in this table is upstream's, and the two --transcripts rows were measured
before the scan became opt-in. Re-measured upstream 2026-08-21: 98.5s and 85.4s
wall (two runs); the ~41s quoted until then was a 2026-08-19 figure that had silently
doubled as the transcript corpus grew. The ~21s default is upstream's single measurement of
the same day. The ClickUp fetch dominates the cheap path, but the per-task sweep scales with
~/.claude/projects, so expect drift on both ā and this host's corpus is not upstream's.
Re-time it rather than quoting it.
Tests
Run all tests (226 collected, measured 2026-08-22). run_all.py exits non-zero on
failure ā but read the Total: N passed, M failed line, not a piped exit code. š“ If that
line is missing at all, the run died ā treat it as a failure, never as "no output". A
sys.exit() from code under test is a SystemExit, which a bare except Exception does
not catch; it used to escape the runner and kill every remaining test file silently (found
by the round-4 mutation sweep, fixed in run_all.py).
CCUA=~/workspace/devrc/scripts/check-clickup-addressed
PYTHONDONTWRITEBYTECODE=1 python3 "$CCUA/tests/run_all.py"
The same files are a pytest target of devrc's gate ā scripts/check-clickup-addressed/tests
in HERMETIC_TARGETS, with its collected-count floor in TARGET_FLOORS
(scripts/run-tests.sh). Both runners see the same 226; run_all.py survives because it
purges __pycache__ and reports an import failure as a FAILURE, which pytest's summary
line does not distinguish as loudly. Raise the floor when you add tests.
š“ The mutation battery lives in the repo now: tests/mutation_sweep.py holds every
mutant as DATA plus a runner (--list, an id filter, --check to fail on a stale one).
Earlier rounds each ran a sweep, reported "0 survived", and threw the driver away ā the
number was then unreproducible from the repo, and the next round re-invented the list and
re-discovered the same sites. A blind re-audit of one such "51 mutants, 0 non-killed" found
seven more at the same delta sites, one of which reverted that round's headline fix.
Extend the list; do not start a new one. Currently 82 rows (81 mutants + a positive
control) + a NULL CONTROL, 0 non-KILLED ā the driver prints 82 mutant(s); non-KILLED: 0.
š“ --check's other job is catching a mutant whose ANCHOR you moved. Measured
2026-08-23: one edit to suppressed_notes silently stranded M49 and M-SCOPE at
NOT APPLIED ā and M-SCOPE is the mutant a blind re-audit once found surviving a green
sweep. --check went red and named both. A NOT APPLIED row reads exactly like a pass in a
long list; only the exit code tells them apart.
š“ That null control is not decoration: it copies the skill, mutates NOTHING and must report
SURVIVED. On this battery's first run every mutant scored KILLED because
test_no_real_identifiers.py cannot resolve the repo root from a temp copy and reddened
them all for free. Read the killer NAME, not just the verdict.
PYTHONDONTWRITEBYTECODE=1 python3 "$CCUA/tests/mutation_sweep.py" --check
Test files:
test_attribution.pyā the 2026-08-19 regression set (cross-task attribution, theno_mentions_foundcontract, self-exclusion, signal precision, PR-ref resolution, ticket/comment disagreements) plus the 2026-08-20 set (D4 prior-run exclusion, the keep-open veto). Readclaude/skills/check-clickup-addressed/reference/validation-history.mdbefore changing any of it ā several tests there are false-positive controls that pass at base by construction and are labelled as such.test_status_and_tiers.pyā the 2026-08-21 round-4 set: the unknown-ClickUp-status blind spot, the deleted proximity tier and the invariant guards that license deleting it, and the entry point's flag parsing/forwarding.test_repo_vocab_and_waiting.pyā the round-5 set: the three distinct repo-resolution failures (+ theKNOWN_REPOSowner ledger), the adjacency marker reaching the actual report, and the waiting-on-a-human flag with its widening controls. š“test_an_unknown_clickup_status_does_not_disable_the_waiting_flagasserts the WAITING flag specifically: a bareassert flagswas green for round 4's announcement instead, and let a mutant rebuilding the D6 blind spot SURVIVE a fully green suite.test_own_reply_answers.pyā the round-6 set (D12): the waiting flag reading your own replies. š“test_collect_computes_the_reply_over_the_UNFILTERED_comment_listexists because a mutation sweep found the wiring uncovered ālatest_reply_byandbuild_recordwere each pinned in isolation while_collect, the only thing that joins them, was not, solatest_reply_by([], my_id)made the whole fix inert against a fully green suite. Pin the SEAM, not only the parts. It also carries the round-7 set: theUNIDENTIFIEDsentinel for an unreadable date on one of YOUR comments, the raw-ms comparison, and the suppression note ā includingtest_main_actually_PRINTS_both_blocks_end_to_end, which drivesmain()and reads STDOUT because deleting its whole print loop left the suite green.test_waiting_corpus.pyā š“ the second labelled corpus, 21 whole RECORDS.test_corpus.pyscores comment TEXT; the waiting flag reads FIELDS, so none of those 49 cases can see it ā and this flag had by then been argued from single anecdotes three rounds running. Verdicts are read off the claim the report makes (WAITING/ANNOUNCE/UNREADABLE/ANSWERED/SILENT), never off whether something fired. Pre-port scores 10/21, shipped 21/21. Same rule as the other corpus: add your motivating case, with the verdict a human would give, BEFORE you change code.test_bounds_and_parsing.pyā the round-8 set. Section 1 IS regression coverage: the scan-less announcement, its one-line-per-run bound, the state-gated open-signals flag, and an end-to-endmain()drive in BOTH modes ā red before the transcript opt-in landed. Everything after it is the other-coverage sentence's SCOPING, the DISPLAY half of the two-ages split, andUNANSWERED_COMMENT_DAYSpinned ON its boundary (14.0) and at a NON-multiple overshoot (20) ā the old fixtures sat at 13 and 30, and 30 is more than 2Ć14, so a doubling mutant passed straight through the gap. ā ļø Those are mutation-coverage guards, not regression coverage: each SURVIVED a full green battery on a tree where the behaviour was already right. The file says so, and says why its red against pre-portmaindoes not count.test_check_completion.pyā windowed signal extraction. ā ļø Its two proximity tests handextract_signals_from_windowsa non-zerodistance, which no production producer emits; they cover the formula, not any reachable path. The reachable premise is pinned intest_status_and_tiers.pyinstead.test_search_sessions.pyā session search, ranking, date filteringtest_recent_comments.pyā ClickUp API parsing, filtering, sortingtest_no_real_identifiers.pyā š“ this repo is PUBLIC. Two pinned LEDGERS over both halves of the skill: every task-ID-shaped token, and every fixture comment author. An unregistered value is red by default, because a synthetic ID and a real one are the same nine characters and nothing can tell them apart by looking. Regenerate the fixture SYNTHETIC ā preserving the shape the case was chosen for ā then write it down. It reads the working tree only, like devrc's four sibling content gates: it says nothing about what is already in git history.