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.
73
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
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 and policy_warnings to empty arrays.
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, sources, feed_failure, 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, and any source whose sources entry reports malformed records — the status values and the counts behind them are the script's contract (scripts/check-cfps-fetch.py docstring). 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 Step 3's feed_failure is true, report a technical failure and finish here — that flag is the branch point, not the warning text; a valid empty feed and a partially usable one are both false.
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."
Geo gate — every source, before Tier 1: pipe every candidate in the working set, as a JSON array of {slug, country, location, city, is_online}, to:
python3 /home/node/.claude/skills/tessl__check-cfps/scripts/geo-gate.py --policy /workspace/group/cfp-geo-policy.jsonIt returns a parallel array of {slug, verdict, country, reason}:
excluded → status: "dismissed", with bot_notes set to its reason (Auto-dismissed: Geo: …).excluded candidate skips Tiers 1–3.user_actioned: true entry is never changed by the geo gate.allowed or unresolved → continue to Tier 1. An unresolved country is never a reason to dismiss.policy_warnings, then run Tiers 1–3 without the geo gate.slug; fix it and rerun.Country resolution and the verdict rules: geo-gate.py docstring.
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 — Jev relevance gate: read the procedure below, resolved from the installed tessl__check-cfps skill root as references/jev-relevance.md:
skills/check-cfps/references/jev-relevance.mdRun its helper on every remaining candidate, including stored open/approved rows. Use the live speaker profile and local relevance criteria. Sessionize's universal API has no descriptions or tags; the helper combines existing metadata with bounded public website meta descriptions.
open → status: "open", bot_notes = the helper's reason.dismiss → status: "dismissed", bot_notes = the helper's substantive reason, including scores.preserve → keep the entry unchanged.model → analyze the candidate using Sonnet 5.5 or newer.For a model case, cite specific evidence in bot_notes; missing evidence stays uncertain. Scheduled new candidates with insufficient evidence stay eligible for discovery without a persisted dismissal or open claim. For an existing row, preserve its prior relevance decision while refreshing verified metadata and travel results. Add uncertain names to research_warnings. There is no automatic relevance default for Sessionize.
Tier 3 covers the full cohort every run. On first rollout, rescore all existing open/approved rows through these same steps; preserve user actions, geo dismissals and Tier 1 approval. A Jev dismiss overrides shown_in_brief stickiness in Step 8. During that initial backfill, an evidenced Sonnet dismissal also overrides stickiness for a row whose prior notes lack Jev relevance v1:; prefix the new substantive notes with Dismissed: Jev relevance v1: Sonnet —. A Jev error or uncertain model judgment never overrides stickiness.
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>"], "policy_warnings": ["<operator policy file that could not be used: diagnostic>"]}
</internal>research_warnings is empty when no candidate lacks topic evidence. policy_warnings is empty when every operator policy file loaded. Both are run-report fields, never persisted CFP fields. 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