Finds open conference CFPs relevant to the user across Java/AI/developer conferences, with persistent sent/dismissed/remind state and source-aware Sessionize verification. NanoClaw per-chat overlay, loaded via containerConfig.additionalTiles.
71
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Process steps in order. Do not skip ahead.
Fetches open CFPs from multiple sources via scripts/check-cfps-fetch.py, applies routing + AI-based relevance analysis in Step 6, and maintains persistent state across sessions. The fetcher owns source-list and blocklist filtering; tier-based routing (including the javaconferences.org auto-approve path) is the agent's work in Step 6.
tessl__nightly-cfp-sync or with scheduled arguments, apply scheduled mode throughout this invocation, including resumed runs.WebSearch, WebFetch, mcp__nanoclaw__fetch_markdown, browser-rendering tools, or delegate web research.The skill's write invariants (dedup-artifact ban, immutable user_actioned, dismissal-reason discipline, last_verified surfacing gate, no-silent-defer, budget-low-is-not-a-defer-reason) and the Step 5 verification-failure protocol (_verify_failed, ⚠️ STALE DATA prefix, caller-visible counts) live in references/contracts.md. Read once, apply throughout.
Run this first, before any other step. This pipeline can be interrupted mid-run by a token-limit continuation. To resume from disk instead of reconstructing the working set from chat history, open (or start) the run's checkpoint store:
python3 /home/node/.claude/skills/tessl__check-cfps/scripts/run-state.py begin{"resume": false} — fresh run. Proceed from Step 2.{"resume": true, "completed": [...]} — a run begun earlier today was interrupted. For each stage already in completed, reload its artifact with run-state.py load <stage> instead of recomputing it, and resume at the first step whose stage is absent.Scheduled override: after begin, invalidate prior pipeline artifacts, ignore its saved completed list, and proceed from Step 2. Abort on a non-zero exit. Initialize this invocation's research_warnings to an empty array.
python3 /home/node/.claude/skills/tessl__check-cfps/scripts/run-state.py invalidate fetch candidates verify working_set verify-evidenceStages, in pipeline order: fetch (Step 3), candidates (Steps 2–4 merge), verify (Step 5 driver), working_set (Steps 5–7, ready for Step 8). After producing each stage's artifact, persist it:
echo '<artifact json>' | python3 /home/node/.claude/skills/tessl__check-cfps/scripts/run-state.py save <stage>Resume is best-effort — stages are idempotent and Step 5 re-verifies the full cohort, so a fresh run is always safe; the store only avoids redoing expensive work. It is per-UTC-day (a continuation on a later day resets). Stage shapes, lifecycle, and the day-boundary reset: references/run-state.md.
python3 /home/node/.claude/skills/tessl__check-cfps/scripts/discover-open-cfps.pyDiscovers new Sessionize open-CFP candidates deterministically (needs the host-injected SESSIONIZE_SPEAKER_KEY; reads /workspace/group/cfp-state.json to skip already-tracked slugs). Do NOT call the Sessionize API or parse its response inline — that is the script's job (jbaruch/nanoclaw-conferences#9). Parse stdout {candidates, counts} and carry candidates into the pool. Abort if the script exits non-zero (an outage must not read as "0 new CFPs"). Do not write to state here. The candidate shape and filter rules are the script's contract (scripts/discover-open-cfps.py docstring).
python3 /home/node/.claude/skills/tessl__check-cfps/scripts/check-cfps-fetch.pyParse JSON output: cfps, warnings, checked_at. Checkpoint: save fetch (the script's stdout) before merging. Then merge Sessionize candidates from Step 2, dedup by slug. Tier-1 auto-approve is NOT guaranteed on name collisions; where you must choose between equivalent rows, keep the one with more complete metadata. Surface warnings at the top of output. Abort if script fails.
Scheduled: use the candidate pool from Steps 2–3 without web gap search. Continue to the checkpoint below, including when a fetch warning suggests web fallback. If both primary feeds report fetch or format failures, report a technical failure and finish here; an empty valid feed is not a failure.
Interactive: read /workspace/trusted/user_professional.md for Baruch's current speaking topics. Construct 2–3 web search queries from his actual topics combined with CFP discovery terms. Add new CFPs not already in the list (dedup by conference name). Apply hard filters (no online/virtual, no excluded locations). Do not apply relevance filtering yet.
Checkpoint: once the full candidate pool is assembled (Steps 2–4 merged and deduped), save candidates (the merged pool) before Step 5.
Interactive JS-rendered CFP pages: use the fallback chain in references/web-fetch-fallback.md for Steps 4, 6, and 7. Do not use that chain in scheduled mode.
Pre-verify: name repair. Before assembling the stored cohort, run the deterministic name backfill so no nameless record reaches Step 6 blind (a record without name is invisible to the priority matcher and the brief — jbaruch/nanoclaw-conferences#23):
python3 /home/node/.claude/skills/tessl__check-cfps/scripts/backfill-name.pyIt guarantees a usable name on every record it can, touching nothing else and never touching user_actioned: true entries (immutability per references/contracts.md); the derivation rules are the script's contract (scripts/backfill-name.py docstring). Surface a non-zero unnamed_remaining or skipped_user_actioned in the run report. Abort on non-zero exit (state file unreadable).
Pre-verify: deadline expiry. Then run the deterministic expiry pass — the single writer of status: "expired" (jbaruch/nanoclaw-conferences#27):
python3 /home/node/.claude/skills/tessl__check-cfps/scripts/expire-cfps.pyIt expires stale non-Sessionize open/approved rows whose deadline has passed, so they leave the verify cohort below instead of being re-blessed every run; eligibility, guards, and the revival path are the script's contract (scripts/expire-cfps.py docstring). Include a non-zero expired count in the run report. Abort on non-zero exit (state file unreadable).
Verify two cohorts:
open/approved entries — every slug in cfp-state.json with status in (open, approved).Routing is source-aware — Sessionize is authority only for source == "sessionize-speaker-api"; non-Sessionize sources are deadline-of-record; entries with no source infer it from the cfp_url host (written back in Step 8). Rules + inference table + backfill: references/source-routing.md.
One deterministic driver does prepare → live per-slug verification → apply in a single invocation, calling the Sessionize API itself (host-injected SESSIONIZE_EVENT_API_KEY). Make the Sessionize round-trip ONLY through this script — never inline — so its large response stays out of context; do not derive slugs, join results, or pick verdicts in prose.
Pass the entries to verify on stdin as a JSON array — one object per new candidate (Steps 2–4) and per stored open/approved row — each {id, cohort: "new"|"stored", cfp_url, source?, slug?}:
python3 /home/node/.claude/skills/tessl__check-cfps/scripts/verify-sessionize.pyIt verifies the Sessionize cohort against the live API (host-injected SESSIONIZE_EVENT_API_KEY) and writes the verify-evidence.json marker Step 8's stamp reads, emitting {prep, results, decisions, summary, non_sessionize, evidence} — the routing, verdict rules, and per-slug failure contract are the script's (scripts/verify-sessionize.py docstring). Checkpoint: save verify (this output). Send non_sessionize ids to the branch below. Apply each decision to the working set:
verified → set deadline to the decision's value, mark _verified_this_run: true, clear the stale markers per references/contracts.md (stale: false, strip the canonical ⚠️ STALE DATA prefix, drop _verify_skipped), and attach the decision's event fields (e.g. expenses_covered) in memory for Steps 6/8.dismiss → status: "dismissed", bot_notes = the decision's bot_notes.drop → drop the new candidate.verify_failed → apply the verification-failure protocol in references/contracts.md.No live API call — the source feed is the authority. Mark _verified_this_run: true on every entry in this branch (new candidates AND stored open/approved) so Step 8 advances last_verified to today. Stored entries additionally: set stale: false, strip any single leading ⚠️ STALE DATA — Sessionize verification failed on prefix from bot_notes (idempotent), and delete _verify_failed if previously set.
Step 5 covers the full cohort each run. See references/contracts.md "Budget-low is not a defer reason."
Tier 1 — javaconferences.org auto-approve: status: "approved", bot_notes: "Auto-approved: javaconferences.org source".
Tier 2 — Blocklist: Check conference name (case-insensitive) against _blocked_prefixes. Match → status: "dismissed", bot_notes: "Auto-dismissed: blocked prefix '[prefix]'".
Tier 3 — AI relevance analysis: Analyze remaining CFPs using all available data — Sessionize description (ground truth), tags, past speakers, audience type, format. Read /workspace/trusted/user_professional.md for Baruch's topics and apply criteria from /workspace/group/RELEVANCE-CRITERIA.md.
bot_notes without inventing evidence.research_warnings. For a new candidate, omit it from the state write without persisting a dismissal; keep it eligible for later discovery. For an existing row, preserve its prior relevance decision and notes while applying this run's verified metadata and travel result. Missing evidence alone never justifies a downgrade. Do not claim the conference is off-topic without evidence.Relevant → status: "open", bot_notes citing specific evidence. Irrelevant → status: "dismissed", bot_notes: "Dismissed: [reason]".
The "lean relevant when ambiguous" latitude applies ONLY when Tier 3 actually ran on the candidate. Tier 3 covers every candidate that reaches it; see references/contracts.md "Budget-low is not a defer reason."
Priority interest tagging (prefilter → arbitrate). First check the policy: if /workspace/group/cfp-priorities.json is absent, empty, or carries no priority_interests (no policy), delete matched_interests from every non-user_actioned open/approved entry you process and skip the rest of this paragraph — no policy ⇒ pin everything. (Don't infer "no policy" from an empty prefilter result; a present policy that simply matched nothing also returns no proposals.)
Otherwise, pass every candidate now open/approved (JSON array of {name, source, bot_notes}) on stdin to the deterministic prefilter:
python3 /home/node/.claude/skills/tessl__check-cfps/scripts/match-priorities.py --priorities /workspace/group/cfp-priorities.jsonIf the prefilter exits non-zero (malformed config → exit 1, malformed records → exit 2), surface its stderr diagnostic and skip priority tagging this run — leave existing matched_interests untouched (don't tag, don't clear). On success it returns a JSON array parallel to the input (each {name, proposed_interests}, same order — join back by position). Then arbitrate per candidate, reading each proposed interest's definition in cfp-priorities.json: drop a proposal the interest's note excludes or the description contradicts; add an interest the CFP clearly matches on content with no hit (e.g. "Confitura" → java). Record the result as matched_interests — no match → []. Never set, change, or delete matched_interests on user_actioned: true entries. Prefilter matching rules: match-priorities.py docstring. note semantics, absent-vs-[], brief partitioning: references/state-management.md.
/workspace/group/travel-schedule.json, extract type: "Trip" entries.open/approved CFP, parse conf_date:
status: "conflict", append "Travel conflict: overlaps with [Trip Name] ([start] – [end])." to bot_notes.Judge exact-date availability for the warning helper in Step 8. Leave warning-string updates to that helper; retain date interpretation and travel-overlap decisions here.
Checkpoint: the working set is now fully decided (verification + relevance + travel applied). save working_set (the in-memory entry set) before the Step 8 write — a continuation here reloads it and writes, skipping Steps 2–7.
Read and execute the full procedure at this path before continuing:
skills/check-cfps/references/write-state.mdResolve it as references/write-state.md relative to this installed skill. It owns the dedup passes, per-entry write priorities, lock-owning commit, schema stamp, evidence-gated freshness stamp, and checkpoint cleanup. Abort on a technical failure; preserve the checkpoint. On freshness-stamper exit 3, follow its invalidation path and report verification: "skipped".
After writing cfp-state.json, emit the run's verification report inside an <internal> block. verification is the freshness stamper's verdict — "live"/"none-required" when it advanced _last_checked, or "skipped" when it exited 3 (no live verification this run):
<internal>
{"checked_at": "<ISO>", "new_candidates_added": N, "existing_verified": N, "existing_verify_failed": N, "verification": "live"|"none-required"|"skipped", "research_warnings": ["<candidate name: missing topic evidence>"]}
</internal>research_warnings is empty when no candidate lacks topic evidence. It is a run-report field, never a persisted CFP field. Surface non-empty warnings in the output; the scheduled caller consumes them as specified in its own skill.
Stale-data guardrail (applied before formatting). Suppress an entry from the brief if:
_verify_failed: true, ORlast_verified is missing or >7 days ago, ORlast_verified with provenance in notes.Stickiness locks in relevance verdicts, not deadline freshness. Suppression is logged to /workspace/group/cfp-suppressed-today.json.
Urgency claims require fresh verification. Only output deadline urgency emphasis (≤48h) when _verified_this_run is true and cfp_end_local is within 48h. Otherwise use plain CFP closes [deadline].
Sort open/approved CFPs by deadline. Group by urgency:
Format:
[emoji] <b>[Conference Name]</b> — [City, Country], [Conference Date]
CFP closes [deadline] ([N days])
Submit: [URL]
[bot_notes — one line]If no open/approved CFPs: return nothing (wrap in <internal>).
Return the formatted, grouped list. Include a brief note if any data sources were unavailable. Dismissed and conflict CFPs are not shown.
If existing_verify_failed > 0, append a short user-visible warning naming the count and the resulting ⚠️ STALE DATA entries.
See references/state-management.md for status values, slug format, user-feedback action table, calibration rules, and state-format example. Schema: /workspace/group/cfp-state.json; criteria: /workspace/group/RELEVANCE-CRITERIA.md.
.tessl-plugin
skills
check-cfps
references
scripts
nightly-cfp-sync