Skip to content
Dennis Lee edited this page Jul 23, 2026 · 15 revisions

CLAUDE.md

Project-level rules for all agents working in this wiki.


Helper scripts

Scripts in helper/ are canonical for their task -- never regenerate their logic inline (python3 -c, heredocs), not even as a one-time workaround. If a required script does not exist, create it first, then call it.

Bookmarks workflow (/wiki bookmarks.json)

Bookmarks file: api/bookmarks.json in the dennislwm/playboard GitHub repository.

  • Live document: new entries are prepended (newest is index 0, oldest is last).
  • Each object fields: href, description, extended, meta, hash, time (ISO 8601 UTC), shared, toread, tags (space-separated).

Cursor file: radar/_bookmark_cursor.json tracks last_presented_time and last_presented_hash of the last presented batch's final item. Never present items whose time is at or before last_presented_time, even if asked to go back. Write it only via python3 helper/update_cursor.py <last_presented_time> <last_presented_hash> — never with an inline heredoc. Call it after presenting a batch, never after fetching (fetch must stay re-runnable).

  1. Fetch next batch using python3 helper/fetch_bookmarks.py [batch_size] from the repo root (reads cursor automatically).

  2. For each item present: timestamp, title, URL, tags, and a radar assessment (proposed quadrant + ring with one-line rationale). Items without a clear radar quadrant may still be presented; if approved, absorb as a standard wiki article (not a blip).

    Presentation format: Group into two sections:

    • Skip — items with no radar applicability
    • Consider — items worth assessing

    Both sections use the original fetch position number (e.g. 1, 2, 6, 8). Format each item as - **N.** Title — rationale (unordered list with bold inline number). Never use markdown ordered list syntax — renderers auto-renumber non-sequential lists, hiding the actual fetch position.

  3. Ask the user which items to approve by number (e.g. 2 8) or elaborate (e.g. elaborate 5) or none. Do not use AskUserQuestion — plain text prompt only.

  4. Before absorbing, restate "Absorbing: [title] (N)" for each approved number to confirm the mapping. Then absorb and update the cursor silently via helper/update_cursor.py (no user approval required for cursor writes).

  5. After absorbing, immediately fetch and present the next batch. Repeat until the user replies stop or no more items remain.

Default batch size is 10. The user may request a different size for the session.

Radar absorption protocol

This wiki is exclusively a technology radar. Articles go in radar/<quadrant>/. Blips persist to radar/_blips.json.

Quadrant folders: radar/techniques/, radar/platforms/, radar/tools/, radar/languages/ (Languages & Frameworks).

Ring determination

Check the Thoughtworks Radar FAQ (https://www.thoughtworks.com/en-sg/radar/faq) as a reference for ring definitions. Propose a ring with rationale and an optional position (inner / center / outer within the ring).

  • Adopt: Recommended for broad use; no significant reservations. Requires confirmed first-person use (Adopt is Trial plus broad, reservation-free use); third-party evidence does not satisfy the gate.
  • Trial: Worth pursuing; requires confirmed first-person use in the user's own production pipeline at least once -- ask the user; third-party evidence does not satisfy the gate.
  • Assess: Worth exploring; no first-person production use yet.
  • Hold: Proceed with caution; do not start new work with this.

Assess vs Trial/Adopt gate: External maturity, reputation, star count, community adoption, "low barrier to trial", or a "documented trial path" describe a technology worth exploring -- that is Assess. Only the user's own confirmed use crosses into Trial or Adopt. If the rationale would open with "first studied via [source]" and argue from the technology's maturity rather than the user's use, the ring is Assess.

Blog sources

When a blog post documents a reusable workflow pattern, absorb it as a Techniques blip -- not as a blog article. The blip name is the technique (the reusable approach); the blog URL is source evidence. Write the article describing the pattern generically; do not make the blog post itself the subject.

Example: a blog documenting a "YOLO + DeepFace face tracking pipeline" yields a "Detection-then-Identity Video Pipeline" Techniques blip, not a blog article.

Article format

Each radar article must include:

  • Frontmatter fields: radar_quadrant, radar_ring, radar_position
  • A ## Radar Assessment prose section explaining the placement in flat, encyclopedic tone.

Audience: Write for a slightly technical product manager. Lead with the problem, describe outcomes rather than mechanics, be specific and verbose enough to convey real value. Never leave scaffolding labels (e.g. "plain language:", "e.g.:") visible in the final prose.

References: Every verifiable factual claim must have an inline anchored markdown link. Use reference-style links ([anchor text][ref]) with all definitions in a ## References section at the bottom. Do not write claims that cannot be sourced.

No duplicate anchors: Each reference anchor may appear only once in the article body (first meaningful use). Subsequent references to the same URL become plain text.

Show structure before writing: Before creating or editing any article file, present the complete final structure as a code block for user approval. Only write the file after confirmation.

Blip schema (radar/_blips.json)

Always read _blips.json before writing — never overwrite other blips. All _blips.json operations must use helper scripts (see Helper scripts above).

Check for duplicate (before creating any article):

python3 helper/check_blip.py <name>

Exits 0 (prints match) if found — confirm coverage is complete and skip creation. Exits 1 if not found — safe to proceed.

Add a new blip (after creating the article):

python3 helper/add_blip.py '<json-string>'

Pass the blip JSON as an inline string — no temp files. It reads _blips.json, appends the new blip, and writes back. Also accepts a file path if the JSON is too long for a single argument. If a /tmp file was created during a session for any reason, delete it before the session ends.

Update an existing blip (ring promotion, description change, new history entry):

python3 helper/update_blip.py '<json-string>'

After any ring change, immediately update the linked wiki article: (1) set radar_ring: frontmatter to the new ring, (2) rewrite the ## Radar Assessment prose to reflect the promotion rationale. update_blip.py only writes _blips.json -- the article is not auto-synced. Treat both as one atomic operation before committing.

Query blips (read-only lookup by name, ring, quadrant, status, or keyword):

python3 helper/query_blips.py --name "Tool A" "Tool B"
python3 helper/query_blips.py --ring Assess --quadrant Tools --verbose
python3 helper/query_blips.py --keyword secret env
{
  "blips": [
    {
      "name": "Display Name",
      "quadrant": "Techniques",
      "ring": "Trial",
      "position": "inner",
      "status": "active",
      "description": "Evergreen 1-2 sentence description of the technology.",
      "wiki_article": "radar/techniques/ArticleName.md",
      "history": [
        {
          "date": "YYYY-MM-DD",
          "ring": "Trial",
          "position": "inner",
          "rationale": "Why this ring and position.",
          "returning": true
        }
      ]
    }
  ]
}
  • position: optional; omit if not specified.
  • returning: only include (set true) when reactivating a previously retired blip.
  • cascaded: optional; set true on the latest history entry when the blip has been reviewed as a cascade source. Written only by helper/mark_cascaded.py (see /wiki cascade), never by hand.
  • New blip: add with status: active and a single history entry.
  • Existing active blip: append a new history entry; update top-level ring and position.
  • Existing retired blip: set status: active; append history entry with returning: true.

Blip retirement

When retiring a blip, set status: retired and ring: retired at the top level, and append a history entry with a reason field. The top-level position is auto-derived from reason by update_blip.py — do not set it manually on retirement.

reason position (auto) Meaning
table-stakes inner Won completely; universally assumed; no longer a deliberate choice
superseded center A better alternative exists and is on the radar
disqualified outer Known bad — abandoned, insecure, or poor quality; actively migrate away
python3 helper/update_blip.py '{"name":"lpass-env","ring":"retired","status":"retired","history":[{"date":"YYYY-MM-DD","ring":"retired","reason":"superseded","rationale":"Replaced by lpassrc."}]}'

Always confirm with the user before retiring any blip.

Ring cascade on promotion

After promoting a blip's ring, run the /wiki cascade <blip-name> analysis (below) to review related blips for implied changes.

_radar.md

radar/_radar.md is a static human-readable summary derived entirely from _blips.json. Regenerate it after every session that touches the radar. Never edit by hand. It is distinct from the on-demand ASCII diagram produced by /wiki radar (see below).

Regenerate with:

python3 helper/generate_radar_md.py

Do not use generate_radar.py or update_radar_md.py — both exit with code 2 (not found).

Format: grouped by quadrant, then by ring (Adopt → Trial → Assess → Hold), retired blips in a separate section. Columns: Blip | Position | Position Rationale | Article.

/wiki cascade [blip-name] command

Reviews related blips for implied ring or position changes when a blip is promoted or newly added.

No argument: Run python3 helper/mark_cascaded.py --pending to list active Trial/Adopt blips awaiting cascade review (those whose latest history entry lacks cascaded: true). Present those as cascade candidates for the user to select from. A blip already cascaded at its current state is skipped until its ring or position changes again, which appends a new unflagged history entry and re-surfaces it automatically.

With blip name: Run a targeted cascade analysis for that blip:

  1. Read the blip's article for wikilinks (explicit related blips).
  2. Run python3 helper/query_blips.py --keyword <domain terms> to find related blips in the same problem space.
  3. Classify each related blip:
Classification Action
Hold Superseded by the named blip; no trial path remains
Retire (superseded / disqualified / table-stakes) Retire with the matching reason; meanings per the Blip retirement table
Strengthen Move position → inner (more conviction, ring unchanged)
Weaken Move position → outer (less conviction, approaching Hold)
Note Document trajectory only; no blip change
Skip Unaffected; different layer or use case
  1. Present findings as a table. User approves per blip.
  2. Apply approved changes using helper/update_blip.py, then regenerate _radar.md.
  3. Mark the cascaded source blip reviewed: python3 helper/mark_cascaded.py "<blip-name>". This sets cascaded: true on its latest history entry so it is skipped by --pending until it next changes. Only the source blip is flagged, never the related blips examined.

Rules:

  • Promote (ring increase) is never a cascade operation — always a separate deliberate decision.
  • Use helper/query_blips.py for all blip lookups.
  • cascaded: true on a history entry means "this blip's current state was reviewed as a cascade source". Never set it by hand — use helper/mark_cascaded.py.

/wiki radar command

Generates an on-demand ASCII 2×2 grid diagram of the current radar. Run python3 helper/radar_diagram.py from the repo root -- it reads radar/_blips.json fresh each time. Never save the output to any file -- it is always generated fresh and distinct from the static _radar.md.

/wiki ladder <name> [blip-name] command

Scaffolds a sibling project wiki when a blip graduates from radar entry to implementation.

  1. Resolve the blip: if blip-name is given, verify it exists via python3 helper/query_blips.py --name "<blip-name>". If omitted, use the most recently discussed blip in the current session -- state which blip was inferred and get confirmation before proceeding. If no blip-name was given and none can be inferred from the session, stop and ask -- this command never builds a ladder untethered from a blip.
  2. Verify no existing wiki: check ../<prefix><name>.wiki does not already exist. If it does, stop and report -- use /wiki ladder <path> instead to audit or extend it.
  3. Resolve the GitHub owner from this repo's own remote (git remote -v, parse the owner segment) -- never hardcode an owner name in the generated file.
  4. Default the numeric prefix to 13, matching every existing sibling implementation project (13pylabel, 13pynanoclip, etc.). The prefix is a local folder-naming convention only -- it never appears in the GitHub repo name (13pylabel local folder maps to github.com/<owner>/pylabel, confirmed via that repo's own git remote -v).
  5. Draft <prefix><name>.wiki/CLAUDE.md from the canonical template at ../02claude-code/.claude/templates/wiki-project-CLAUDE.md, substituting every <name>, <owner>, and <prefix> placeholder directly:
    • Project context: the blip's description, its wiki article link (https://github.com/<owner>/playradar/wiki/<ArticleFilename>), and the sibling implementation repo reference (https://github.com/<owner>/<name> -- no prefix)
    • ADR format link: https://github.com/<owner>/playradar/wiki/ADR
    • The template's create scaffold/check scaffold subcommands and session-start command line are complete -- substitute placeholders only, no rewriting. Drop domain-specific sections carried from older projects (e.g. 13pylabel's add-table/check-table); domain-specific commands get added later, once a repeatable workflow actually emerges.
  6. Present the full final prefix + folder name + file content as a code block per the existing "show structure before writing" rule -- this is also where the user confirms or overrides the 13 prefix default. No separate prefix-confirmation step.
  7. On confirmation: mkdir the folder and write the file. Do not git init or configure a remote -- that is left to whatever agent works in the new folder later.

/wiki ladder <path> command

Audits an existing wiki's CLAUDE.md against the canonical template, and extends it in the same flow if gaps are found and confirmed. One command, not a separate check-then-create pair -- follows the same discover -> present -> confirm -> write pattern used everywhere else in this file.

  1. Verify <path>/CLAUDE.md exists. If not, stop and report -- nothing to audit.
  2. Determine applicability: does the target have real ongoing implementation surface (multiple scripts, an active pipeline, a spec/plan document) that the canonical template would meaningfully track? If not -- personal-wiki pattern by design (e.g. 20itinerary.wiki, which intentionally uses the generic /wiki skill taxonomy instead) -- report "not applicable" and stop here. Never treat template-absence as a gap on a wiki that was never meant to have it.
  3. If applicable, read the canonical template at ../02claude-code/.claude/templates/wiki-project-CLAUDE.md and the target's CLAUDE.md side by side. Compare:
    • Presence of Requirements.md / Decisions.md + decisions/ / Conventions.md / Tests.md / Deferred.md / Implementation.md / Home.md -- or functional equivalents already doing that job under a different name (e.g. an existing spec doc with task/status tables)
    • Presence of create scaffold / check scaffold / triage / add req / add test subcommands in the target's own CLAUDE.md
    • Any project document (spec, plan, schema reference) that exists in the repo but isn't surfaced or pointed to from CLAUDE.md
    • Wording drift from the canonical template's phrasing not explained by a documented project-specific reason
  4. Present findings as a gap table (Gap | Canonical expectation | Target's actual state), then the proposed CLAUDE.md-only additions as a diff: a wiki-structure table naming what each canonical file is for, an updated session-start command line, and pointers to any project document or tool not currently surfaced. Per "show structure before writing," this is the confirmation point.
  5. This command only ever writes the target's CLAUDE.md. It never creates Requirements.md, Decisions.md, Tests.md, or any other file -- bulk-generating those from existing content is premature scaffolding. Those files come into existence later, on their own triggers:
    • Requirements.md / Tests.md: created on first real add req / add test call for this wiki (the command creates the file with a header row if it doesn't exist yet, then appends the row).
    • Decisions.md / Conventions.md / Deferred.md / Implementation.md / Home.md: no dedicated creation command -- they come into existence the first time the "Wiki artifact placement" ladder actually routes real content to them during real work, per that section's rungs. Declining the confirmation in step 4 is a valid, complete outcome -- the audit stands on its own.

Article naming convention

Article filenames use the title in CamelCase without spaces. For tools whose display names contain dots, slashes, @, or other non-alphanumeric characters, convert the full name to CamelCase for the filename only.

  • Example: tool named lat.md → filename LatMd.md
  • The name field in _blips.json and the title: frontmatter field always retain the original display name.

External tool research

Before absorbing an article about an external tool, verify key claims from live sources during the research phase, before drafting -- especially API capabilities, open issues, and production signals. Do not write claims that have not been verified from a live source.

  • Use gh api repos/<owner>/<repo> for GitHub metadata and open issues.
  • Use WebFetch for API documentation (OpenAPI specs, developer portals).
  • Preferred pipeline: /lens first (source fetching, production readiness signals, open bugs, API constraints), then absorb via /wiki (article creation, blip registration, _radar.md regeneration). When the draft is approved, tell the wiki skill to absorb it directly (bypassing the lens file-write step).

Web search

WebSearch (web_search_20250305) returns API Error 400 on claude-sonnet-4-6 -- the tool type is not supported at the model level.

Workaround: use WebFetch on https://html.duckduckgo.com/html/?q=<url-encoded-query>. Returns plain HTML with search results. Google via WebFetch returns an error page. DuckDuckGo HTML is confirmed working.

Use the WebFetch tool directly for this -- never Bash with curl. Bash/curl web fetch calls are rejected by the user.

AskUserQuestion tool limit

The AskUserQuestion tool has a hard maximum of 4 options per question. Five or more options throw InputValidationError at runtime. If more than 4 options are needed, issue a second AskUserQuestion call in the same response for the remaining options — do not defer to the next turn.

ponytail

ponytail (v4.8.3) is installed as a Claude Code plugin at full intensity. Available each session without activation:

  • /ponytail-audit -- whole-repo over-engineering scan; offer after significant helper script additions
  • /ponytail-review -- diff-scoped review; available before any commit

Clone this wiki locally