SuperAPI AI Directory

zettel-builder

Office & productivity

View repository

Updated 10-16 06:09

About this skill

zettel-builder

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)

  1. 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.
  2. "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.
  3. The value of a link lies inwriting outwhy it is connected. Blind linking equals not linking. Every link must carry a reason field (one line).
  4. 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.
  5. 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:

  1. Source normalization: convert the material into Markdown main text + source metadata (type/ref/timestamp).

  2. 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>.jsonl state the reason (otherwise it counts as information-compression failure)
    • For the detailed reading process see references/voice-alignment.md Step B.2
    • enumeration does not count as a judgment(three principles, four types), unless each item stands alone as a paragraph and contains argumentation
  3. value restatement: for each card, invoke references/voice-alignment.md 's protocol — read references/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...".

  4. 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.

  5. 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.npy cosine, 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
      • 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.
    • 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-character YYYYMMDD-HHMM pure timestamp.Does notcontain title text
    • aliases(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
  6. Image migration (v1.4.0): if source is raw markdown containing ![](...) , first run python3 scripts/migrate_images.py --card-id <id> --source <raw-path> to copy the images to assets/<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).

  7. 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.

  8. 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).

  9. 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:

  1. Status check: read _queue/ current backlog; read _state/last_scan.json;read _state/mode.json to determine the current mode(bootstrap or steady)。
  2. Batch limit(v1.3.0 mode-aware):
    • bootstrap mode: N=20 (high-speed inventory processing period, challenging the long session token budget)
    • steady mode: N=5 (daily processing, event-driven trigger)
    • Read _state/mode.json in thresholds.bootstrap_batch_size / steady_batch_size, and fall back to the default value on error.
  3. 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
  4. Inspect along the way: run a lightweight version of Mode 3 inspect, and if a ready article cluster is found, send a Telegram reminder.
  5. 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 (see references/scan-mechanical.md cursor ownership section for details).
  6. 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:

  1. Run scripts/build_index.py:rebuild the tag/entity/link index.
  2. Run scripts/embed_lite.py --include-published:also refresh the embeddings of cards/ and published-articles, for use in overlap detection.
  3. 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 / adjacent 0.6-0.75 / clean <0.6)
    • kind:core(densest subgraph) or whole(entire connected component)
  4. LLM proposition:for each newly ready cluster, replace the suggested_title placeholder with a declarative judgment sentence (not a noun phrase).
  5. Deduplication:compare against _state/cluster_history.json,filter out clusters that have already been suggested (unless density has risen significantly).
  6. Report(v1.3.0 protocol): Telegram summary + each cluster contains the overlap_class marker:
    • 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:

  1. Read cluster:get the cluster-id from _state/pending_articles.json , and load the member cards.
  2. Gap check:if the cluster contains a gap marker, ask the user whether to first invoke deep-research to fill in cards (invoke /deep-research to go through Skill routing, topic = gap description, after returning, feed back as new cards + re-run cluster readiness determination).
  3. Outline synthesis:sort into an outline by the cards' link relations (not by time).
  4. 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.
  5. Feedback loop:after the article is published (user manually notifies or wiki-sync detects a new published-source), mark the corresponding card's cluster_history as published, and add to the card's frontmatter produced_article: <wiki-path>。
  6. 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:

  1. build_index.py rebuild tag/entity/link/term index
  2. embed_lite.py incrementally refresh BGE embeddings
  3. scan_mechanical.py detect new additions in raw/ → write _queue/
  4. cluster_inspect.py recompute ready/near_ready
  5. 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:

  1. 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)
  2. 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.
  3. 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)
  4. 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
  5. 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 ![](../images/...) ,scripts/migrate_images.py copy 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.py queue item adds the source_images field. Old raw-source cards (v1.3 and earlier) are archived to cards/_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.md Step 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.json next pending → run /deep-research → store ~/zettel/raw/deep_research/<proposal_id>__<gap_type>/ → Mode 1 ingest splits into multiple cards (source.type=deep-research)→ update top3_outlines.json 's deep_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.py generates graphical HTML (SVG relationship graph + LLM outline + gap badges + recommended actions + GitHub blob card links). Pages: https://your-github-user.github.io/zettel/。Oven write top3_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 write hands 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.
  • 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.json switched to prefix cards/_archived to 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.md Step 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.md completely 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.md Step B.5. Also add cleanup cron zettel-tech-cleanup-once, Oven reviews existing cards/ and archives technical detail cards to cards/_archived_technical_v18/。
  • v1.7.0 (2026-05-20) — original timestamp。frontmatter source.created_at required (can be missing for chat); body ## 原文要点 section first line italic annotation *原文时间:<ts>*. Source: wiki-getnote reads its frontmatter created_at, wiki-article checks raw/metadata/article-dates.json。backfill_source_timestamp.py now 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 (serendipity tag), 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.py now 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.json records the current state. bootstrap N=20, steady N=5.zettel-bootstrap-hourly cron runs hourly until the queue is drained; manually openclaw cron disable <id> switch to steady.
    • published-overlap:embed_lite.py --include-published also embed ~/Dropbox/cn_articles_published/all/*.md .cluster_inspect.py for 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.sh Step 6: in steady state, when new content appears in wiki (.new-content-trigger) immediately openclaw cron run <event-driven-scan-id>, no need to wait for the next cron round.
  • 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.