Let cardsgrow themselves: material in → split cards → connect edges → inspect → assemble text. The user retains decision-making power at each branching point, but does not need to manually create cards one by one.
Core idea (read through before any modification)
- Atomization is for recombining, not for making the word count small. The hard standard for judging whether a card is qualified:Apart from context, can you six months later understand it independently? If yes, it is qualified, even a few hundred words is fine. If no, no matter how short it is, it is garbage.
- "Restating in your own words" is not synonym replacement. It must produce a new judgment: what does this view conflict with that I already know? What are the boundary conditions? Do I believe it? Without this step, restating is just slower copying.
- The value of a link lies inwriting outwhy it is connected. Blind linking equals not linking. Every link must carry a
reasonfield (one line). - Articles do not "grow themselves" — articles emerge from dense clusters along existing writing directions. This skill helps you organize materials for an established direction more efficiently; it does not decide what you should write. This is an honest boundary.
- Triggering has two layers: the mechanical scan is run offline by systemd (pure Python, does not split cards, only lists to-dos); the agent scan is triggered by the user or
/schedule(actually splitting cards + restating values + connecting edges).
See references/philosophy.md(why it is designed this way + the difference from topic-inspiration).
Path default
| Parameter | Default | Description |
|---|---|---|
zettel_root |
~/zettel/ |
Card repository root directory, independent git repository |
wiki_root |
~/wiki/published-articles/ |
Upstream material source |
raw_articles |
${wiki_root}/raw/articles/ |
Published article source (241+ articles) |
raw_getnote |
${wiki_root}/raw/getnote/ |
GetNote note sync (1974+) |
published_source |
~/Dropbox/cn_articles_published/ |
Published article upstream (deduplication comparison) |
Hard rules for card format
See the complete specification in references/card-format.md. One-line summary:
---
id: YYYYMMDD-HHMM-<kebab-标题>
status: fleeting | literature | permanent
created: <ISO8601>
source: {type: ..., ref: ...}
tags: [...]
entities: [...]
links: [{id: ..., reason: ...}]
voice_passed: true|false
---
# 标题(直接陈述一个判断,不是名词短语)
正文(150-800 字,可超,以脱离上下文可读为准)。
Never:
- put multiple independent judgments into one card at the same time (split into two cards)
- Write the title as a noun ("atomization principle") instead of a judgment ("the purpose of atomization is recombinability")
- Links only list ids without writing reason
- Copy raw original text into a card (literature note only cites the path + a short excerpt)
Four modes
Mode 1: ingest — single feeding
Usage:/zettel ingest <来源>. The source can be:
- A piece of text pasted into the conversation
- A file path (
/zettel ingest /path/to/note.md) - A URL (go through markdown-proxy to fetch the main text first)
- Several messages above in this conversation ("split the conversation above into cards")
Steps:
-
Source normalization: convert the material into Markdown main text + source metadata (type/ref/timestamp).
-
Atomic splitting (strengthened in v1.13.0: long texts must become multiple cards):
- Identifyall independent judgments, one card each. Judgment markers: "X is not Y, but Z", "the real reason for X is...", "the boundary between X and Y is...", "X on the surface ... in reality ..."
- a wiki-article long-form piece must be read through in full to find all candidates, typical word count to card count: short piece 2-3 cards / medium-long 3-6 cards / long piece 6-10 cards / very long 10-15 cards
- If an article > 1500 characters yields only 1 card,mustin
processed/<date>.jsonlstate the reason (otherwise it counts as information-compression failure) - For the detailed reading process see
references/voice-alignment.mdStep B.2 - enumeration does not count as a judgment(three principles, four types), unless each item stands alone as a paragraph and contains argumentation
-
value restatement: for each card, invoke
references/voice-alignment.md's protocol — readreferences/voice-snapshot.md(your own values/style snapshot)'s core clauses, and rewrite the card's body.Key: it is not changing the writing style, it is producing a new judgment. If the original text quotes someone else, the restatement must add "I agree/disagree/partially agree, because...". -
link candidates: run
scripts/link_candidates.py --card-id <new-id>, to get the top-10 candidate cards. For each candidate, the agent judges whether it is a real connection + writes a one-line reason. No connection, no forcing. -
dedup + linking (v1.6.0 agent-judged): embedding only doesrecall,the final judgment must have the agent read the full text。
- recall stage(embedding): compute candidate card embeddings, compare with
_index/embeddings.npycosine, take:- nearest neighbors top-20 (by sim descending)
- far-distance top-5(randomly drawn from cards with sim<0.5, key — prevents semantic local clustering)
- judgment stage(must Read the full candidate card):
- For near-neighbor candidates: Oven reads the full text and judges "is this really the same judgment?".Do notautomatically skip based on sim.
- truly the same judgment → dedup-skip, record
{action:"dedup-skip", existing:<id>, agent_reason:"..."} - superficially similar but substantively different → write a new card, butmust automatically link to that similar card, relation + reason make the difference clear
- truly the same judgment → dedup-skip, record
- For far-distance candidates: Oven reads the full text and judges "is there a non-obvious cross-domain connection?". A hit establishes a link, relation usually
extend/counter, reason explains the cross-domain bridge.
- For near-neighbor candidates: Oven reads the full text and judges "is this really the same judgment?".Do notautomatically skip based on sim.
- Neverdirectly auto-skip with sim ≥ 0.85 — the agent must finish reading and judge
- Neverselect links only from near-neighbor candidates — examine at least 5 far-distance ones to prevent clustering 5.5. filename + id generation (v1.9.0, key): before writing to disk determine filename and frontmatter:
id(frontmatter)= 13-characterYYYYMMDD-HHMMpure timestamp.Does notcontain title textaliases(frontmatter)= the full H1 title as the first line (Obsidian double-link friendly, changing the title does not break links)filename=<short-id>-<主语-主结论>.md, slug ≤ 20 Chinese characters, first word the subject, English lowercase hyphenated- For the full rules see
references/card-naming.md - links (frontmatter
links:+ body[[]]) always reference the short id
- recall stage(embedding): compute candidate card embeddings, compare with
-
Image migration (v1.4.0): if source is raw markdown containing
, first runpython3 scripts/migrate_images.py --card-id <id> --source <raw-path>to copy the images toassets/<id>/, and record the path mapping. In the subsequent body, reference key images with the new paths (the ones that support this card's judgment; no need to migrate all of them). -
Write to disk (v1.6.0 self-contained + substantive references + Obsidian backlinks):
cards/<id>.md, body structure:## 原文要点section (blockquote fully migrates the 200-500 word original text, including images)## 我的判断section (take a side + boundaries + must naturally cite ≥1 related card)## 相关卡片section (Obsidian backlink index)
Key (v1.6.0): substantive references。
## 我的判断cannot just be an isolated statement, it must be in the bodynaturally citeat least one related card:- Wrong (only lists entries in the ## Related Cards section):
## 相关卡片\n- [[X]] — support: ... - Right (integrated into the body of ## My Judgment):
...如 [[X]] 所论 A 边界在 B,但本卡聚焦的是 C。[[X]] 强调测量,本卡关心如何处置——这是同一光谱上的两端。
The citation must be substantive (state "why it's related" + "points of difference"), not mechanically adding
[[]]. The validator checks the## 我的判断section has at least 1[[]], otherwise voice_passed=false.## 相关卡片section continues to serve as the index (for scripts like cluster_inspect), and the validator verifies the counts match on both sides. -
git push hard requirement(v1.2.0): after writing all cards,mustrun
cd ~/zettel && git add -A && git commit -m "feat(ingest): <一句话摘要 N 张卡>" && git pull --rebase origin main && git push origin main. Do not rely on the systemd timer as a fallback. A push failure must be reported explicitly (common causes: no network, remote conflicts). -
Report: list the new cards + each one's link status + the dedup-skip count + whether the push succeeded.
Mode 2: scan — process the mechanical queue
Usage:/zettel scan(or triggered by OpenClaw cron / event-driven trigger).
Prerequisite:scan_mechanical.py is run hourly by systemd, writing raw/ newly added files into _queue/*.json. Agent scan processes these queue items.
Steps:
- Status check: read
_queue/current backlog; read_state/last_scan.json;read_state/mode.jsonto determine the current mode(bootstraporsteady)。 - Batch limit(v1.3.0 mode-aware):
bootstrapmode: N=20 (high-speed inventory processing period, challenging the long session token budget)steadymode: N=5 (daily processing, event-driven trigger)- Read
_state/mode.jsoninthresholds.bootstrap_batch_size/steady_batch_size, and fall back to the default value on error.
- For each queue item:
- Read the source original text (literature note only cites the path, does not copy the original text)
- Go through ingest's Step 2-5 (card splitting + restatement + linking + write to disk)
- After processing, move the queue item to
_state/processed/<date>.jsonl
- Inspect along the way: run a lightweight version of Mode 3 inspect, and if a ready article cluster is found, send a Telegram reminder.
- Update
_state/processed/<date>.jsonl(only touch agent scan's own progress).Nevermodify_state/last_scan.json— that is mechanical scan's raw cursor, and agent scan writing to it would cause newly added raw files to be skipped (seereferences/scan-mechanical.mdcursor ownership section for details). - git push hard requirement(v1.2.0):
cd ~/zettel && git add -A && git commit -m "feat(scan): <N 张新卡 + Y 项处理>" && git pull --rebase origin main && git push origin main. Cross-machine sync with zero latency. A push failure must be reported explicitly.
Mode 3: inspect — patrol the card pool
Usage:/zettel inspect(or the lightweight version runs automatically at the end of scan).
Purpose:identify three types of signals — article-ready clusters / orphan cards / gaps.Does not automatically produce output,only gives suggestions.
Steps:
- Run
scripts/build_index.py:rebuild the tag/entity/link index. - Run
scripts/embed_lite.py --include-published:also refresh the embeddings of cards/ and published-articles, for use in overlap detection. - Run
scripts/cluster_inspect.py:output a JSON report, each cluster containing:- member id list + density + voice_ratio
- gaps:counter_gap / case_gap / evidence_gap / time_gap (determined based on the enumeration of link relations within the cluster)
- published_overlap(new in v1.3.0): semantic overlap with published articles — top-3 candidates +
overlap_class(covered≥0.75 /adjacent0.6-0.75 /clean<0.6) - kind:
core(densest subgraph) orwhole(entire connected component)
- LLM proposition:for each newly ready cluster, replace the
suggested_titleplaceholder with a declarative judgment sentence (not a noun phrase). - Deduplication:compare against
_state/cluster_history.json,filter out clusters that have already been suggested (unless density has risen significantly). - Report(v1.3.0 protocol): Telegram summary + each cluster contains the
overlap_classmarker:covered:⚠️ Red — "topic already covered by <published article title>, suggest skipping or clarifying a new angle"adjacent:Orange — "topic adjacent to <published article title>, need to explain the difference"clean:Green — "clean new topic, can recommend /zettel write"
For detailed cluster readiness criteria see references/cluster-detection.md. For the published-overlap algorithm and threshold rationale see references/published-overlap.md。
Mode 4: write — assemble clusters into an article
Usage:/zettel write <cluster-id> or /zettel write , then select the most ready cluster.
Steps:
- Read cluster:get the cluster-id from
_state/pending_articles.json, and load the member cards. - Gap check:if the cluster contains a
gapmarker, ask the user whether to first invoke deep-research to fill in cards (invoke/deep-researchto go through Skill routing, topic = gap description, after returning, feed back as new cards + re-run cluster readiness determination). - Outline synthesis:sort into an outline by the cards' link relations (not by time).
- Hand off to writing Skill:invoke your own writing Skill (e.g.,
/your-writer), input = outline + main text of member cards + linked related wiki concepts/.Key:this skill does not write itself, it only assembles the material. - Feedback loop:after the article is published (user manually notifies or wiki-sync detects a new published-source), mark the corresponding card's
cluster_historyaspublished, and add to the card's frontmatterproduced_article: <wiki-path>。 - git push hard requirement(v1.2.0): if there are changes to cluster_history or card frontmatter,
cd ~/zettel && git add -A && git commit -m "chore(write): <cluster-id> handed off" && git pull --rebase && git push。
Scheduling integration (two layers)
Layer 1: systemd (no LLM, hourly)
scripts/zettel-hourly-sync.sh is triggered hourly by zettel-sync.service(systemd user unit), chained after wiki-sync.service (After= + Wants=), ensuring raw/ has been refreshed.
Only does work without LLM:
build_index.pyrebuild tag/entity/link/term indexembed_lite.pyincrementally refresh BGE embeddingsscan_mechanical.pydetect new additions in raw/ → write_queue/cluster_inspect.pyrecompute ready/near_ready- git pull/push fallback (to prevent LLM session from missing push)
Layer 2: OpenClaw cron(LLM session,daily/weekly)
~/.openclaw/cron/jobs.json two jobs in, target=Oven(workspace-openai,GPT-5.5,@your-openclaw-bot):
| Job | Frequency | What it does |
|---|---|---|
zettel-daily-scan |
0 9 * * * |
Digest _queue/ pending items accumulated by mechanical scan, run Mode 2 once, Telegram summary |
zettel-weekly-inspect |
0 10 * * 0 |
Sunday deep dive: LLM proposes theses for ready clusters, interprets gaps, Telegram recommends /zettel write candidates |
Why OpenClaw and not Hermes: OpenClaw cron natively supports payload.kind: agentTurn(wakes up an LLM to run a conversation), Hermes cron is only script-driven (runs Python scripts). zettel's scan/inspect needs an LLM session,architecturally only OpenClaw can express it directly。
See references/scan-mechanical.md(systemd layer) + references/openclaw-cron.md(Oven layer).
Mode 5: propose — prompted essay (v1.12.0)
Usage:
- Imperative:
/zettel propose <seed-text> - Natural language (to Oven): "write one around X", "inspired by this idea of X, find some material from my notes"
Difference from Mode 4 write: Mode 4 is bottom-up (ready cluster → article); Mode 5 is top-down (user seed → select materials from the network to form a cluster → article).
Steps:
-
Python recall:
python3 scripts/propose_seed.py --seed "<text>"- Compute the BGE embedding of the seed
- Do cosine with cards/ embeddings, take top-30 candidates
- Write
_state/proposal_<ts>.json(including seed + candidates + metadata)
-
Agent adjudicates + assigns roles(must Read the full text of each candidate card):
- support: this card can serve as evidence supporting the seed
- counter: this card opposes / qualifies the seed
- case: this card is a concrete case of the seed
- context: this card provides background / prior history
- unrelated: seems relevant but actually isn't, discard It's not that high sim means include — you must look at the actual content to judge.
-
Write the proposal document:
~/zettel/_drafts/proposal_<ts>.md- Seed original text (introduction)
- Role-assigned card list (each entry
[[<card-id>]] — <role>: <一句话相关性说明>) - Draft title (a synthesizing thesis based on seed + candidate cards, a declarative judgment, not a noun phrase)
- Suggested outline (treat the seed as the thesis, organize the candidate cards into a chain of argument that carries it forward)
- Gap annotation (if there is a hole in the main support chain, deep-research can be triggered to add cards)
-
Report back:
- Telegram brief + proposal_id
- After the user reviews
_drafts/proposal_<ts>.md, they can:/zettel write proposal_<ts>: hand off to your own writing Skill to produce a draft/zettel propose ...: swap the seed and start over- Manually edit the proposal: add/remove cards / change the outline
-
git push hard requirement: after the proposal document is written, cd ~/zettel && git add/commit/push, visible across machines
Trigger keywords(Oven automatically routes to Mode 5 when it receives these on Telegram):
/zettel propose <seed>- "write one around X" / "inspired by this idea of X" / "prompted essay X" / "write based on this idea"
Relationship with other skills
| Skill | Relationship |
|---|---|
| Your writing Skill | Mode 4 hands off; Mode 1 restatement phase reuses its values + style-guide (maintained as a snapshot in references/voice-snapshot.md ) |
| Topic/topic-inspiration type Skills | Complementary: That one is about finding topics from gaps (top-down), while this skill works on long cards from existing material (bottom-up). The two can chain: topic output → convert to deep-research → card into zettel |
| deep-research | Mode 4 gap patching; for invocation method see references/deep-research-handoff.md |
| Your notes/collection sources(such as GetNote, Readwise) | Upstream sources (landed into raw/ via wiki-sync etc.) |
| Chitchat/idea extension Skill(such as getseed) | No overlap: That type extends chitchat into linear outputs; this skill accumulates a card network. Can be chained: after linear output, convert to ingest |
Things not done (preventing boundary slippage)
- Do not auto-publish: ready clusters only send Telegram suggestions, waiting for user
/zettel write - Do not change your writing style rules: when restating, read directly from
references/voice-snapshot.md(your pre-filled values/style snapshot), do not copy business style in this skill - Do not act as a GetNote mirror: literature notes cite raw/ paths, do not copy the original text
- Do not solve "what to write": this skill helps you organize more efficiently in a given direction, it does not decide the direction for you (this is an honest boundary)
- Local embedding for semantic recall (v1.1.0):BGE-small-zh-v1.5 via
fastembed, ~90MB cached in~/.cache/fastembed/(not committed to git); TF-IDF as fallback path. Whether it is truly connected in the end is still judged by the agent
On failure
| Symptom | Troubleshooting |
|---|---|
| ingest cannot find a judgment, can only list | The material itself is a list/fact set, not suitable for a permanent note; consider only putting it into fleeting/ |
| Link candidates are all low-relevance | Check whether _index/ is expired, run scripts/build_index.py --rebuild |
| scan repeatedly processes the same file | Check whether _state/last_scan.json 's mtime has been reset by git;processed/<date>.jsonl whether it is missing writes |
| Cluster is ready but no Telegram received | Check whether _state/cluster_history.json has already marked that cluster (deduplication logic) |
| Value restatement becomes a word-swapping game | agent rereads references/voice-alignment.md, this protocol explicitly requires producing a new judgment; otherwise it does not count as voice_passed |
Version
- v1.4.0 (2026-05-20) — Self-contained protocol: the body must contain the
## 原文要点block (blockquote fully carrying over 200-500 words of original text, not a 30-word summary) + the## 我的判断section.Image migration: when the original material contains,scripts/migrate_images.pycopy the image to~/zettel/assets/<card-id>/, and the card body uses the new path for references. Still readable when detached from source.ref serves as the hard test.scan_mechanical.pyqueue item adds thesource_imagesfield. Old raw-source cards (v1.3 and earlier) are archived tocards/_archived_v13/, and raw is re-enqueued to Oven to be redone under the new protocol. - v1.13.0 (2026-05-20) — wiki-article multi-card splitting. Users found that at v1.12, 82 articles produced only 83 cards (average 1.01), over-compressing the information. New protocol: long articles must be read through + all atomic judgments found + card count produced according to word count (short 2-3 / medium-long 3-6 / long 6-10 / very long 10-15). An article of 1500+ words producing only 1 card must state the reason in the processed jsonl. Rules are in
references/voice-alignment.mdStep B.2. The 82 articles already split into 1 card are not forced to be redone for now; after bootstrap finishes, the user decides whether to backfill. - v1.16.1 (2026-05-21) — Deep-research background gap-filling worker. New cron
zettel-deep-research-worker(disabled by default, manual trigger or cron schedule). Flow: read_state/deep_research_pending.jsonnext pending → run/deep-research→ store~/zettel/raw/deep_research/<proposal_id>__<gap_type>/→ Mode 1 ingest splits into multiple cards (source.type=deep-research)→ updatetop3_outlines.json'sdeep_research_status→ render HTML → push → Telegram. Trigger words "/zettel deepresearch" / "fill a gap", etc. - v1.16.0 (2026-05-21) — Graphical TOP3 + GitHub Pages deployment。
render_top3_html.pygenerates graphical HTML (SVG relationship graph + LLM outline + gap badges + recommended actions + GitHub blob card links). Pages: https://your-github-user.github.io/zettel/。Oven writetop3_outlines.json+deep_research_pending.json, Python renders HTML. - v1.15.0 (2026-05-21) — Multi-view cluster detection. See above for details.
- v1.12.0 (2026-05-20) — Mode 5 proposition-based writing + evidence graph priority。
- Mode 5 propose: user provides a seed (case/argument/question), Python recalls top-30 candidate cards → agent reads cards to determine relevance + assigns roles (support/counter/case/context)→ writes
_drafts/proposal_<ts>.md→ after user review/zettel writehands over to the writing Skill. Top-down "proposition-based writing", complementary to Mode 4 (bottom-up generating articles from ready clusters). - Evidence graph protocol: wiki-article source defaults to
migrate_images.py --skip-first --include-remote. The first image is usually the title image (cover/decorative), skip it; evidence images (idx 1+) are R2 remote URLs, use the URL directly in the card body, do not copy locally. Image selection criteria: evidence images (screenshots/data charts/comparison charts) preferred, title images not.
- Mode 5 propose: user provides a seed (case/argument/question), Python recalls top-30 candidate cards → agent reads cards to determine relevance + assigns roles (support/counter/case/context)→ writes
- v1.11.0 (2026-05-20) — Cold-start source gatekeeping + Obsidian archive hiding + YAML double-quote fix. During the bootstrap phase source.type only accepts wiki-getnote / wiki-article (other sources go through manual Mode 1 ingest). Archived 5 chat-source historical cards to
cards/_archived_bootstrap_chat_v1_11/(they are demo cards from zettel-builder's own smoke test, not conforming to the cold-start protocol)..obsidian/app.jsonswitched to prefixcards/_archivedto one-click hide all archive directories, Obsidian no longer displays broken / archived cards. Fixed the double-quote nesting YAML in card 1504 (single quotes on the outside). - v1.10.0 (2026-05-20) — Timeliness filtering + extraction of principles that transcend time. Determine whether source.created_at is more than 12 months from now. Old material only extracts judgments at the principle/rule/methodology level,does not extractjudgments bound to specific models/tools/prices/features. Added an abstraction technique (raising "GPT-4 writes code" to the level of "high-capability models expand developers' modes of output"). Full rules are in
references/voice-alignment.mdStep B.4. AI iterates extremely fast; this prevents the zettel network from being diluted by dead information from "years ago". - v1.9.2 (2026-05-20) — Reverted to the single form id = filename stem(user feedback that v1.9.0/v1.9.1 alias resolution mechanism was unstable + added cognitive burden). The aliases field has been removed. Wikilinks directly use the full filename stem, natively resolved by Obsidian.
references/card-naming.mdcompletely rewritten. - v1.9.1 (2026-05-20) — short ids also used as aliases to fix Obsidian dangling links.has been superseded by v1.9.2。
- v1.9.0 (2026-05-20) — attempted to decouple filename / id, introducing short ids + aliases.has been superseded by v1.9.2(over-engineering).
- v1.8.0 (2026-05-20) — session-review filter. When session-review type material (determined by tag/filename/structure) enters a card,only extract the user leveljudgments (needs/values/preferences/corrections/boundaries),do not extract the technical level(problem description/solution/tool details/Codex fix). Rule in
references/voice-alignment.mdStep B.5. Also add cleanup cronzettel-tech-cleanup-once, Oven reviews existing cards/ and archives technical detail cards tocards/_archived_technical_v18/。 - v1.7.0 (2026-05-20) — original timestamp。frontmatter
source.created_atrequired (can be missing for chat); body## 原文要点section first line italic annotation*原文时间:<ts>*. Source: wiki-getnote reads its frontmattercreated_at, wiki-article checksraw/metadata/article-dates.json。backfill_source_timestamp.pynow backfilled for all existing 54 non-chat cards (20 wiki-article + 34 wiki-getnote). - v1.6.0 (2026-05-20) — Agent-judged links + Serendipity emergence mechanism. Changes:
- dedup/link judgment changed to agent reading the full text,no longertrusting cosine to auto-skip. Recall expanded to top-20 nearest neighbors + top-5 distant (sim<0.5 random); Oven decides only after reading.
## 我的判断body must naturally cite at least 1 related card (substantive[[]]inline), not just## 相关卡片section listing items. Validator verifies.- New cron job
zettel-serendipity-daily: 3 times per day (08:00 / 14:00 / 20:00) randomly pick 20 mutually unconnected cards, Oven reads the full text to find potential associations, and on a hit establishes a link (serendipitytag), with reason explaining the cross-domain bridge. - Serendipity → cluster emergence linkage: after each serendipity run, immediately call
cluster_inspect. Newly ready clusters (especially those containing serendipity links) immediately get a high-priority Telegram push "⚡ Emergence: a new theme is rapidly saturating".
- v1.5.0 (2026-05-20) — Full inventory consumption mode(replaces the previous misuse of "mark already-scanned + only process increments"). Changes:
scan_mechanical.pynow by default enqueue raw/getnote + raw/articles all (241 published articles are no longer skipped; judgments in published articles are also valuable raw material, flowing back into the network after dedup)- Three-tier semantic dedup(protocol in Mode 1 Step 5): before writing to disk, embed and compare; max_sim ≥0.85 skip, 0.7-0.85 force link, <0.7 create new. One source can correspond to zero or multiple cards
- bootstrap_runner.sh daemon: continuously takes batches from the queue and feeds them to Oven, no longer rate-limited by cron intervals; on quota/rate-limit use exponential backoff (2/4/8/16/32 min); after the queue stays empty for 1 hour, automatically switch to steady (disable bootstrap cron + enable event-driven + update mode.json), send Telegram "bootstrap complete"
- v1.4.0 (2026-05-20) — Self-contained protocol + image migration + voice_passed validator (detailed above)
- v1.3.0 (2026-05-20) — Dual-state scheduling (bootstrap → steady), published-articles embedding dedup, event-driven trigger.
- Dual-state:
_state/mode.jsonrecords the current state. bootstrap N=20, steady N=5.zettel-bootstrap-hourlycron runs hourly until the queue is drained; manuallyopenclaw cron disable <id>switch to steady. - published-overlap:
embed_lite.py --include-publishedalso embed~/Dropbox/cn_articles_published/all/*.md.cluster_inspect.pyfor each ready cluster compute centroid → cosine match → top-3 candidates +overlap_class. When Mode 3 LLM proposes a topicmust read overlap, Telegram summary uses red/orange/green three-color marking. - event-driven:
zettel-hourly-sync.shStep 6: in steady state, when new content appears in wiki (.new-content-trigger) immediatelyopenclaw cron run <event-driven-scan-id>, no need to wait for the next cron round.
- Dual-state:
- v1.2.0 (2026-05-20) — Mode 1/2/4 explicitly after writing cards
git add + commit + push(no longer relying on systemd as a fallback). Added OpenClaw cron orchestration: Oven (workspace-openai, GPT-5.5) takes over the daily scan + weekly inspect, sent natively via Telegram through @your-openclaw-bot. - v1.1.0 (2026-05-20) — link candidates switched to BGE-small-zh-v1.5 embeddings (semantic, primary) + tag/entity/linked-neighbor structural signals (secondary); the TF-IDF fallback path is retained. mechanical scan no longer budgets candidate_links; that computation has been moved into the agent path.
scripts/inspect.py→cluster_inspect.py(to avoid a stdlib name collision). - v1.0.0 (2026-05-20) — initial version. TF-IDF + 4-factor scoring.