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.
76
95%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Referenced from check-cfps SKILL.md ("State Management"). Full schema lives at /workspace/group/cfp-state.json; relevance criteria at /workspace/group/RELEVANCE-CRITERIA.md.
open, approved, dismissed, conflict, sent, remind, expired. User wording like "submitted to [conf]" normalizes to status: sent; submitted is not a distinct status.
expired has a single writer: scripts/expire-cfps.py (check-cfps Step 5 pre-verify), which closes out stale non-Sessionize open/approved rows past their deadline-of-record (jbaruch/nanoclaw-conferences#27); Sessionize rows are instead dismissed by the live-verify "MISSED" path. Expiry overrides shown_in_brief stickiness (same basis as the Step 8 confirmed-closed exception) but never touches user_actioned: true rows. Revival: expired slugs are not hidden by the fetcher's state filter, so a CFP re-listed upstream with an extended deadline re-enters as a candidate and is rewritten from the fresh verdict.
{conference-name-slug}-{year} — lowercase, spaces/punctuation to hyphens.
Every row MUST also set user_actioned: true.
| User input | Action |
|---|---|
| "sent to [conf]" / "submitted to [conf]" | status: sent, user_actioned: true, update updated |
| "not interested [conf]" / "skip [conf]" | status: dismissed, user_actioned: true |
| "remind me [N] days before deadline [conf]" | status: remind, remind_before_days: N, user_actioned: true |
| "remind me about [conf] in a week" | status: remind, remind_before_days: 7, user_actioned: true |
| "show again [conf]" | remove entry from cfp-state.json (clears user_actioned) |
Record Baruch's dismissal reason in baruch_notes. When a deadline expires with status: "open", ask if he submitted and record the outcome. When Baruch restores a bot-dismissed CFP, note it in baruch_notes for future calibration.
matched_interests is an orthogonal axis to status (jbaruch/nanoclaw-admin#308). Step 6 judges each open/approved CFP against the operator-owned priority list and records the matched interest ids. The morning brief partitions on this field: a non-empty list OR an absent field → pinned brief; an explicit empty list [] → separate non-pinned follow-up. The absent-vs-empty distinction is load-bearing — never normalise absent to [].
Priorities live at /workspace/group/cfp-priorities.json — operator-owned, same provenance as RELEVANCE-CRITERIA.md, NOT shipped in the plugin. The interest taxonomy is data, not code: edit this file to change what's priority (drop Java, add Rust) with no skill or schema change. Each interest carries id, label, keywords, sources, and an optional free-text note. Shape:
{
"priority_interests": [
{ "id": "ai", "label": "AI", "keywords": ["AI", "ML", "LLM", "agent", "GenAI"], "sources": [] },
{ "id": "java", "label": "Java", "keywords": ["Java", "JVM", "Kotlin", "Spring"], "sources": ["javaconferences.org"] },
{ "id": "agentcon", "label": "AgentCon (first-world only)", "keywords": ["AgentCon"], "sources": [],
"note": "Priority only when held in a first-world country (US, Canada, EU/EEA + UK, developed Asia). Elsewhere is NOT priority." }
]
}keywords and sources are hints for the Step 6 judgment, not literal match gates. The optional note is authoritative operator intent the judgment honors — use it for rules keywords/sources can't express (geo scoping, source-scoped topic filters); a note constraint can EXCLUDE an otherwise-matching keyword/source hit. Missing or empty file ⇒ Step 6 tags nothing AND clears matched_interests from the non-user_actioned entries it processes ⇒ the brief pins everything (no policy, no split). The clear matters: an entry that earned [] under a prior config would otherwise stay demoted after the config is removed.
Tagging is two-stage (jbaruch/nanoclaw-admin#308): the deterministic prefilter scripts/match-priorities.py consumes CFP records and emits each one's proposed_interests; Step 6's LLM then arbitrates — REMOVE what a note excludes or the description contradicts, ADD content-only matches with no proposal (e.g. "Confitura" → java). The prefilter never applies note exclusions or no-hit additions; those are judgment. The matching predicates are the script's contract — see the match-priorities.py top-of-file docstring.
Every CFP record carries its own schema_version (currently 1, introduced with matched_interests in jbaruch/nanoclaw-admin#308) so a shape change is auditable per coding-policy: stateful-artifacts.
check-cfps. Its Step 8 write phase runs scripts/stamp-schema-version.py — a deterministic, idempotent stamper that sets schema_version on EVERY record (incl. user_actioned, dismissed, sent), so one run reliably migrates the whole file (replacing LLM hand-stamping). schema_version is the one owner-metadata field exempt from the "preserve user_actioned entirely" rule; the user-owned decision fields stay untouched. The same run reconciles matched_interests (tag, clear, or preserve per the rules above).morning-brief-cfp.py): a non-owner reader. Per stateful-artifacts, a record whose schema_version != 1 (including a legacy record with no version) is "no usable prior state" — skipped, not surfaced, until check-cfps migrates it. The reader never migrates; it tallies skipped records to stderr. --mark-shown only touches records that passed the gate and preserves their schema_version in place.dedup-by-url.py, system-audit.py): operate on version-independent structural fields (cfp_url/slug dedup, script inventory), not the matched_interests shape, so this version does not gate them.schema_version on any record; until check-cfps next runs (nightly housekeeping or a manual invocation) the reader surfaces no CFPs. check-cfps's deterministic stamper brings every record to 1 on its next run, after which the reader resumes. No data rewrite beyond the stamp is needed — absent matched_interests on a version-1 record is already the pinned default._last_checkedTop-level _-prefixed config field (not a CFP record). The UTC ISO-8601 instant the check-cfps pipeline last ran to completion, independent of whether any record changed — a run that re-verified everything and changed nothing still advances it. It answers "did the pipeline run?", which per-record updated/last_verified cannot: those stay put when nothing changed, so a healthy idle pipeline looks identical to a dead one if you read them alone.
check-cfps Step 8, via scripts/stamp-last-checked.py — the sole writer, run on every successful pipeline pass. Not LLM hand-written.nightly-cfp-sync-cursor.json last_run records when the wrapper last fired (gates cadence); _last_checked records when check-cfps itself last ran, including direct (non-wrapper) invocations. Both exist on purpose._last_checked (older than the longest expected gap between runs) means the pipeline genuinely stopped running — a real alert.{
"_last_checked": "2026-06-13T11:36:13Z",
"_blocked_prefixes": ["devopsdays", "blockchain", "web3", "crypto", "gaming", "unity3d", "unreal engine", "salesforce", "sap", "sharepoint"],
"all-things-open-2026": {
"schema_version": 1,
"status": "open",
"name": "All Things Open 2026",
"city": "Raleigh, NC, USA",
"conf_date": "Oct 18-20",
"deadline": "2026-03-31",
"cfp_url": "https://allthingsopen.org/call-for-papers",
"updated": "2026-03-31",
"last_verified": "2026-03-31",
"matched_interests": ["java"],
"bot_notes": "General open-source dev conf with broad audience; typically has Java/JVM content"
}
}.tessl-plugin
skills
check-cfps
references
scripts
nightly-cfp-sync