| name | content-pipeline |
| description | {{AGENT_NAME}} content pipeline - editorial topic selection, draft writing, Korean humanization, review-ready publishing |
Content Pipeline Skill
Aggregator pattern: ์ด skill์ content๋ฅผ ์์งํ์ง ์์.
monitoring โ watch, don't prompt.
- Synthesize in batches. One cycle produces blog draft + X thread + LinkedIn.
- Track narrative arc. Posts accumulate into a story. Season/episode structure keeps continuity.
- Start simple. One agent profile + cron job + one tracking file. No multi-stage pipeline unless proven necessary.
Modes
Mode A: Aggregator (external โ blog)
Collect from external sources (trend-harvester, SEO-harvester), select topics, assign to content-writer via kanban.
Suitable for: trend posts, SEO-optimized evergreen, tool roundups.
Mode B: CMO Agent (internal work โ content)
Single autonomous agent profile (content-manager) observes Drew's recent work and produces multi-format content. Runs daily at 12:00 KST via cron.
Trigger: Cron (0 12 * * *), deliver to Discord #content-channel.
Profile: ~/.{{AGENT_NAME_LOWER}}/agents/content-manager.md (deepseek-v4-pro, tools: terminal, file, search, session_search, web)
Skill prerequisites: content-pipeline (this skill), SVG knowledge, Excalidraw, Mermaid.
Suitable for: build logs, troubleshooting deep-dives, architecture decisions, project retrospectives.
See references/cmo-agent-mode.md for the full implementation guide.
Mode B requires these knowledge base files (in P4-cortex/content/):
brand-guide.md โ brand positioning, voice, audience
glossary.md โ project terms ({{AGENT_NAME}}, M-LOG, PDC...)
content-inventory.md โ published/drafted content for dedup
narrative_arc.md โ episode tracking, season structure, continuity
The agent reads all four at the start of every cycle before gathering context.
Editorial North Star
YOUR_DOMAIN์ ์๋ ๋ด์ค ๋ธ๋ก๊ทธ๊ฐ ์๋๋ผ Drew๊ฐ AI, ๋๊ตฌ, ์ฝ๋, ๊ธ์ฐ๊ธฐ, ์์คํ
์ค๊ณ๋ฅผ ํตํด ์ด๋ป๊ฒ ์ฌ๊ณ ํ๊ณ ๋ง๋๋์ง๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฐ์ธ ์์
์ค์ด๋ค.
๊ณต๊ฐ ํ๋ณด๊ฐ ๋๋ ๊ธ์ ๋ฐ๋์ ์๋ ๋ฑ์์ ๋ง์กฑํด์ผ ํ๋ค:
public-worthy content = ๊ธฐ๋ก + ํด์ + ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ํต์ฐฐ
๋จ์ํ ์์ง ๊ฒฐ๊ณผ, ์ถ์ ์์, ํ๋ ๋ณด๊ณ , ๋งํฌ ์์ฝ์ ๊ณต๊ฐ draft๋ก ๋ง๋ค์ง ์๋๋ค. ๊ทธ๋ฐ ํญ๋ชฉ์ raw ๋๋ archive์ ๋จ๊ฒจ๋ ๋์ง๋ง content board task๋ก ์น๊ฒฉํ์ง ์๋๋ค.
Editorial State Machine
raw source
โ editorial_screen
candidate
โ enough Drew-angle + reader value
draft โ status: draft โ Quartz EXCLUDE
โ human review
published โ status: published โ Quartz INCLUDE
โ rework needed
in_review โ status: in_review โ Quartz EXCLUDE
โ no longer useful
archived โ status: archived โ Quartz EXCLUDE
content-pipeline์ candidate โ draft๊น์ง๋ง ์๋ํํ๋ค. published ์ ํ์ ์ฌ์ฉ์๊ฐ ์ง์ ๊ฒํ ํ ๋ค์๋ง ํ๋ค.
Triggers
- Mode A (Aggregator): Cron job 3์๊ฐ๋ง๋ค (
0 */3 * * * KST)
- Mode B (CMO Agent): Cron job daily (
0 12 * * *), content-manager agent profile
- One story per cycle. SILENT if nothing new since last run.
- Material-driven cadence: backlog clears one post per day, not batched.
See references/cmo-agent-mode.md for the full Mode B implementation.
Approach & Decision-Making
When making technical/content recommendations for Drew (site platform, theme, tools, services):
-
Try first, evaluate later. Propose a concrete implementation path, apply it to the current environment, and test. Do NOT decide based on imagination, brand reputation, or hypotheticals alone. Retreat only if the tested approach proves genuinely inefficient.
-
Research, don't pretend. If you don't have first-hand experience with a tool or technology, say "let me search for that" โ not "I think X is good." The user will call out fake expertise immediately. When asked for a recommendation:
- Search the web for current options
- Test the top candidate in the user's environment
- Report what you found and what worked
- Don't generate comparison tables from hearsay or brand recall
-
Don't fear change. Changing platforms (Quartz โ WordPress), tools, or approaches is not inherently bad. Evaluate the migration cost against the benefit honestly, and if the direction makes sense, execute it rather than listing objections.
-
When asked for a recommendation, search first. Before saying "I recommend X," search for current options, pick 2-3 concrete candidates, install/test the most promising one, and report results. Let real testing guide the decision, not your training data.
-
Don't list objections for a direction the user is already committed to. If the user says "let's use WordPress" or "let's run Huly on NAS," don't produce a comparison table of alternatives. Instead:
- Say "let's check if it works" and test it
- Surface blocking issues only after the test fails, not before
- Default to "yes, let's try that" over "here are the trade-offs"
These rules apply any time the user asks "which X should I use" or "what do you recommend."
Patching & Conventions
When updating or extending skills, follow these conventions:
Reference File Updates
references/ files preserve session-specific detail. When a one-time setup or workflow is discovered, add a reference file rather than bloating SKILL.md. Each reference file starts with a one-line summary of what it covers.
- Scripts live in
scripts/. Anything the agent should run rather than hand-type. Scripts are documented in SKILL.md with their usage example.
- Templates live in
templates/. Boilerplate configs, scaffolding, starter files meant to be copied and modified.
When to Add vs. Update
- Add a reference file when a new technique, tool, or workflow was discovered during the session (e.g., fixing NAS SSH, setting up WordPress MCP).
- Update SKILL.md when the core workflow changes (e.g., a new Mode, a step order change, a new image type).
- Update memory when a USER preference or environment fact is learned (e.g., "user prefers trying first over analysis").
- Don't save if nothing was learned that a future session would benefit from.
Phase 1: Source Tally (Aggregator Mode)
์ง์ ์์งํ์ง ์์. ๊ธฐ์กด cron output 3๊ฐ๋ฅผ ์ฝ์.
1. Trend Harvester โ analyzed/keep
์์น: ~/.{{AGENT_NAME_LOWER}}/P4-cortex/growth/trend-harvester/analyzed/keep/
ํ์: JSON (item{name,description,url,source}, total_score, decision)
ํํฐ: decision == "keep", scored_at ์ต๊ทผ 48์๊ฐ
์ ๋ณ: ์์ 5๊ฐ โ topic ํ๋ณด
ls -t ~/.{{AGENT_NAME_LOWER}}/P4-cortex/growth/trend-harvester/analyzed/keep/*.json | head -10
JSON ํ์ฑ:
import json, pathlib
keep_dir = pathlib.Path("~/.{{AGENT_NAME_LOWER}}/P4-cortex/growth/trend-harvester/analyzed/keep")
files = sorted(keep_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)[:10]
for f in files:
d = json.load(open(f))
item = d["item"]
print(f"- {item['name']}: score={d['total_score']:.1f}, source={item['source']}")
2. SEO Harvester โ report.json
์์น: ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/knowledge/seo-articles/report.json
ํ์: JSON (articles[{title,url,keyword,score}])
์ ๋ณ: score โฅ 0.7, keyword ๋ช
ํํ ๊ฒ ์์ 3๊ฐ
cat ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/knowledge/seo-articles/report.json | python3 -c "
import json, sys
d = json.load(sys.stdin)
articles = d.get('articles', [])
scored = [(a, a.get('score', 0)) for a in articles if a.get('score', 0) >= 0.7]
for a, s in sorted(scored, key=lambda x: -x[1])[:5]:
print(f'- {a[\"title\"]} | keyword={a.get(\"keyword\",\"?\")} | score={s:.2f}')
"
3. Activity Logger โ kanban tasks (last 24h)
์์น: {{AGENT_NAME_LOWER}}_tasks.db (board=default, trigger_source=activity_logger)
์ฟผ๋ฆฌ: SELECT title, body FROM tasks
WHERE trigger_source = 'activity_logger'
AND status = 'completed'
AND created_at > datetime('now', '-24 hours')
์ ๋ณ: conversation insight ๊ด๋ จ ๊ฒ (implement/build/create ๋ฑ ํฌํจ) ์์ 3๊ฐ
import sqlite3, pathlib
db = pathlib.Path.home() / ".{{AGENT_NAME_LOWER}}/P2-hippocampus/kanban/state/{{AGENT_NAME_LOWER}}_tasks.db"
conn = sqlite3.connect(str(db))
rows = conn.execute("""
SELECT title, body FROM tasks
WHERE trigger_source = 'activity_logger'
AND status = 'completed'
AND created_at > datetime('now', '-24 hours')
ORDER BY created_at DESC
LIMIT 10
""").fetchall()
for title, body in rows:
print(f"- [{title}] {body[:100] if body else ''}")
conn.close()
Phase 2: Topic Selection
์ธ source์์ ํฉ์ฐ ์ต๋ 3๊ฐ topic ์ ๋ณ. ๊ธฐ๋ณธ๊ฐ์ 0๊ฐ๋ค. ์ถฉ๋ถํ ์ข์ ํ๋ณด๊ฐ ์์ผ๋ฉด [SILENT]๊ฐ ์ฌ๋ฐ๋ฅธ ๊ฒฐ๊ณผ๋ค.
| ์ถ์ฒ | Topic ์ ํ | Prefix |
|---|
| Trend Harvester | [draft-trend] | ๊ธฐ์ ๋ํฅ |
| SEO Harvester | [draft-seo] | ๊ฒ์ ์ต์ ํ |
| Activity Logger | [draft-conversation] | ์์
insight |
Editorial Gate
๊ฐ ํ๋ณด๋ 0~2์ ์ผ๋ก ํ๊ฐํ๋ค. ์ด์ 7์ ์ด์๋ง content board task๋ก ๋ง๋ ๋ค.
| ๊ธฐ์ค | 0์ | 1์ | 2์ |
|---|
| Drew-angle | Drew์ ์์
/ํ๋จ๊ณผ ๋ฌด๊ด | ์ฝํ๊ฒ ์ฐ๊ฒฐ๋จ | Drew์ ์์
๋ฐฉ์, ๋๊ตฌ, ํฌํธํด๋ฆฌ์ค์ ์ง์ ์ฐ๊ฒฐ |
| Insight | ์์ฝ/์์ ์์ค | ๊ด์ฐฐ์ ์์ผ๋ ์ผ๋ฐํ ์ฝํจ | ๋
์๊ฐ ์ฌ์ฌ์ฉํ ์ ์๋ ํ๋จ ๊ธฐ์ค์ด ์์ |
| Evidence | ์ถ์ฒ/๋งฅ๋ฝ ๋ถ์ถฉ๋ถ | ๊ธฐ๋ณธ ์ถ์ฒ ์์ | ์ถ์ฒ + ์ค์ ์์
๋งฅ๋ฝ ๋๋ artifact ์์ |
| Portfolio value | ์ง๋๊ฐ๋ ๋ฉ๋ชจ | blog note๋ก๋ ๊ฐ๋ฅ | portfolio/insight ์ถ์ ๋จ๊ฒจ๋ ๊ฐ์น ์์ |
| Specificity | ์ผ๋ฐ๋ก , ํค์๋ ๋์ด | ์ด๋ ์ ๋ ๊ตฌ์ฒด์ | ๊ตฌ์ฒด์ ์ฌ๋ก, ํ๋ก์ ํธ, ๋๊ตฌ, ์คํจ/๊ฒฐ์ ํฌํจ |
์๋ reject ๊ท์น:
- ๋จ์ ์ ํ/๋๊ตฌ ์ถ์ ์์ฝ
- Drew๊ฐ ์ง์ ์จ๋ณธ ํ์ ์ด๋ ํ๋จ์ด ์๋ ์ธ๋ถ ํธ๋ ๋
- SEO keyword๋ง ์ข๊ณ ๊ด์ ์ด ์๋ ์ฃผ์
- ์ด๋ฏธ ์ต๊ทผ 30์ผ ๋ด ๊ฐ์ ์ฃผ์ฅ์ผ๋ก ๋ค๋ฃฌ ์ฃผ์
- "AI๊ฐ ๋๋จํ๋ค/์ํํ๋ค" ์์ค์ ์ผ๋ฐ๋ก
source๋ณ ์ฐ์ ์์:
[draft-conversation]: Drew์ ์์
์์ ๋์จ insight๋ฉด ์ฐ์ .
[draft-trend]: {{AGENT_NAME}}, YOUR_DOMAIN, agent tooling, creative coding, publishing system๊ณผ ์ฐ๊ฒฐ๋ ๋๋ง.
[draft-seo]: ๊ฒ์ ์ ์
๋ณด๋ค ์ฌ์ดํธ ์ ์ฒด์ฑ์ ๋ง๋ evergreen ์ฃผ์ ์ผ ๋๋ง.
์ต๋ 3๊ฐ. ํ์ง > ์๋.
Phase 3: Kanban Task Creation
์ ๋ณํ topic๋ง๋ค content board์ task ์์ฑ:
kanban_create(
title=f"[draft-{type}] {topic_title}",
body=f"""## Topic
{topic_description}
## Editorial Decision
- score: {score}/10
- publish_intent: blog | insight | portfolio | archive
- Drew-angle: {why_this_matters_to_drew}
- reusable_insight: {one_sentence_reader_value}
- reject_if_missing: if this cannot show Drew's judgment, archive instead of drafting
## Content Source
- source: {source_name}
- collected_at: {YYYY-MM-DD HH:MM}
## ๊ธ์ฐ๊ธฐ ๋ฐฉํฅ
- ํค: writing-style-guide.md hybrid approach ๋ฐ๋ฅด๊ธฐ
- ํค: ํด๋จธ๋๋ ๋งํฌ (๊ธด ํ๋ก์ฐ, Bold ๊ฐ์กฐ, "๋น์ " ์ง์ ํธ์นญ, 1์ธ์นญ "์ "/"๋")
- SEO: ๊ธฐ์กด ๋ฐฉ์ ์ ์ง (aliases, ํด์ํ๊ทธ, SEO ํค์๋ ์น์
)
- ์ ๋ชฉ: ์ง๋ฌธ ๋๋ provocative statement
- ๋์
: Bold ํ
ํ ๋ฌธ์ฅ โ 10~20๋ฌธ์ฅ ํ๋ก์ฐ
## Frontmatter
title: {title}
type: document
space: concept
tags: [blog, {category}]
aliases: ['/blog/{slug}']
created: {YYYY-MM-DD}
status: draft
links:
- "[[@identity/persona/writing-style-guide]]"
## ์์ฑ ์ ๋ฐ๋์ ํ์ธํ ๊ฒ
1. forbidden.patterns grep โ 0๊ฑด
2. Bold ์น์
๊ฐ์กฐ 2~4๊ฐ ์กด์ฌ
3. "๋น์ " ์ง์ ํธ์นญ + 1์ธ์นญ "์ "/"๋" ํฌํจ
4. ๋ณธ๋ฌธ ๋ ์ง (X์ X์ผ) ์์
5. SEO ํค์๋ 3~7๊ฐ, ํด์ํ๊ทธ 8~14๊ฐ
6. aliases in frontmatter
7. ๊ธฐ๋ก + ํด์ + ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ํต์ฐฐ์ด ๋ชจ๋ ์์
""",
board="content",
trigger_source="content_pipeline",
priority=1,
idempotency_key=f"{YYYY-MM-DD}-{slug}",
)
Title prefix ๊ท์น:
- ํธ๋ ๋ ๊ธฐ๋ฐ:
[draft-trend]
- SEO ํค์๋ ๊ธฐ๋ฐ:
[draft-seo]
- ๋ํ/๊ฒฝํ ๊ธฐ๋ฐ:
[draft-conversation]
Draft ํ์ผ ์์น: memories/insights/YYYY-MM-{slug}.md
- ์:
memories/insights/2026-05-gemini-cli-shutdown.md
- Obsidian์์ ์ง์ ํ์ธ ๊ฐ๋ฅ:
~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/
- Kanban ๋์๋ณด๋ ์ฐ๋: body์
## Draft ํ์ผ ์์น ์ ๋๊ฒฝ๋ก๊ฐ ์์ผ๋ฉด ๋์๋ณด๋ ์นด๋์ ๐ Obsidian ๋งํฌ๊ฐ ์๋ ์์ฑ๋จ. ์นด๋ ํด๋ฆญ โ Description ํญ ํ๋จ "๐ Open in Obsidian" ๋ฒํผ ํด๋ฆญ โ Obsidian์์ draft ํ์ผ ์ด๋ฆผ.
- YOUR_DOMAIN์
/blog/{slug} ๋๋ /blog/YYYY/{slug} ๊ฒฝ๋ก๋ก Quartz์ ์ํด ๊ณต๊ฐ๋จ. Raw monthly log๋ ๊ณต๊ฐํ์ง ์์.
Phase 4: Draft Writing ({{AGENT_NAME}} Worker)
kanban-dispatcher๊ฐ ready task๋ฅผ worker์ ๋ฐฐ๋ถ.
4-1. Task Claim
kanban_claim(task_id, ttl_seconds=1800)
4-2. Research (if needed)
Topic ๊ด๋ จ ์ ๋ณด๊ฐ ์ถฉ๋ถํ์ง ์์ผ๋ฉด web search๋ก ๋ณด๊ฐ. ์ถ์ฒ: URL, source-date ๋ช
์.
4-3. Draft ์์ฑ
memories/insights/์ Markdown ํ์ผ ์์ฑ.
writing-style-guide.md ์ ์ฉ (hybrid approach):
- ํค: ํด๋จธ๋๋ ๋งํฌ (๊ธด ํ๋ก์ฐ, Bold ๊ฐ์กฐ, "๋น์ " ์ง์ ํธ์นญ, 1์ธ์นญ "์ "/"๋")
- SEO: ๊ธฐ์กด ๋ฐฉ์ ์ ์ง (aliases, ํด์ํ๊ทธ, SEO ํค์๋ ์น์
, frontmatter)
** ReefWatch ์คํ์ผ ๊ธ ๊ตฌ์กฐ (๊ธฐ์ ์ฌํ ๊ธ์ฉ):**
ReefWatch ์ํฐํด (dev.to/siiddhantt/building-reefwatch)์ 7๊ฐ์ง ํต์ฌ ํฌ์ธํธ๋ฅผ ์ ์ฉํ ํ
ํ๋ฆฟ:
- ๋ฌธ์ ํ๋ ์ด๋ฐ โ ๊ณต๊ฐ๋๋ ์ํฉ๋ถํฐ ์์. Bad: "์ด ๊ธ์ ~์ ๋ํด ์์๋ด
๋๋ค." Good: ๊ตฌ์ฒด์ ๊ฒฝํ/์ํฉ โ ๋ฌธ์ ๋ฐ๊ฒฌ
- ๊ฐํ ์ฃผ์ฅ (Bold) โ ๊ธฐ์กด ๋ฐฉ์์ ํ๊ณ๋ฅผ Bold๋ก. ์: "But that is not triage. That is a polished to-do list."
- ๋์์ธ ์ ์ฝ (emphasis) โ ํต์ฌ ์์น์ emphasis๋ก ๊ฐ์กฐ. ์: "The design constraint from the start was simple: no evidence, no answer."
- One-sentence ์์ฝ (blockquote) โ ํ ๋ฌธ์ฅ์ผ๋ก ์ ์ฒด ์ ์. ๋ธ๋ก์ฟผํธ๋ก ์๊ฐ์ ์ผ๋ก็ชๅบ
- ์ด๋ฏธ์ง ๋ฐฐ์น โ "์ค๋ช
์ ์ ๋ณด์ฌ์ฃผ๊ธฐ" โ ๊ฐ๋
์ค๋ช
์ง์ ์ ์๊ฐ ์๋ฃ. Obsidian:
![[image.png|๋๋น]]
- "What This Guide Builds" ํ
์ด๋ธ โ ๊ธ ๋์
๋ถ์ outcomes๋ฅผ ํ
์ด๋ธ๋ก ์ ์. ๋
์๊ฐ ์ฒซ ์ค์บ์์ ํ๋จ
- Build Path / ๊ตฌ์กฐ ํ
์ด๋ธ โ ์์๋ฅผ ์๊ฐํ. ๋ค๋จ๊ณ ์ปดํฌ๋ํธ ์ค๋ช
์ ํจ๊ณผ์
** ReefWatch ํ์ฅ ํ
ํ๋ฆฟ:**
# {์ ๋ชฉ: ์ง๋ฌธ ๋๋ provocative statement}
{์ปค๋ฒ ์ด๋ฏธ์ง: ![]() โ 16:9 landscape, ๊ธ ์ ์ฒด๋ฅผ ๋ํํ๋ ์๊ฐ์ }
**{๋ฌธ์ ํ๋ ์ด๋ฐ โ Bold ํ
}**
{๊ตฌ์ฒด์ ์ํฉ ๋ฌ์ฌ. ๋
์๊ฐ ๊ณต๊ฐํ ์ ์๋ ๊ฒฝํ/๊ด์ฐฐ}
**{๊ฐํ ์ฃผ์ฅ โ Bold ํ ๋ฌธ์ฅ}**
{๊ธฐ์กด ๋ฐฉ์์ ํ๊ณ ๋๋ ๋ฌธ์ ์ ๋ณธ์ง}
{๋ถ์ ํ๋ก์ฐ: 10~20๋ฌธ์ฅ. ๊ฒฝํ โ ๊ด์ฐฐ โ ๋ถ์}
**{์ค๊ฐ ํต์ฌ ๊ฐ์กฐ โ Bold}**
{๊ตฌ์ฒด์ ํด๊ฒฐ์ฑ
๋๋ ์ธ์ฌ์ดํธ}
{์ด์ด์ง๋ ํ๋ก์ฐ}
> {One-sentence ์ ์ โ ๋ธ๋ก์ฟผํธ๋ก ํ ๋ฌธ์ฅ ์์ฝ}
## What This Guide Builds
| ๋น์ ์ ์ด๊ฒ์ ํ ์ ์๊ฒ ๋๋ค |
|---|
| {outcome 1} |
| {outcome 2} |
| {outcome 3} |
{๊ตฌํ/์ค๊ณ ์น์
}
{์ด๋ฏธ์ง: ![]() โ ํ๋ก์ฐ/์ํคํ
์ฒ ๋ค์ด์ด๊ทธ๋จ, ์ค๋ช
์ ์ ๋ฐฐ์น}
{์ด์ด์ง๋ ์ค๋ช
}
**{๋ง๋ฌด๋ฆฌ ๊ฐ์กฐ โ Bold ํ ๋ฌธ์ฅ}**
{๊ฒฐ๋ก ๋๋ ํ๋ ์ ๋}
---
**์ด๋ฐ ๋ถ๋ค๊ป ์ถ์ฒ**
- {ํ๊ฒ ๋
์}
**SEO ํค์๋**: {ํค์๋ 3~7๊ฐ}
**#ํด์ํ๊ทธ**: #{ํ๊ทธ1} #{ํ๊ทธ2} ...
** ReefWatch ์คํ์ผ ๊ธ์ฐ๊ธฐ ์์:**
- ๋ฌธ์ ์ํฉ ๋ฌ์ฌ (Opening) โ Bold ํ
- ๊ธฐ์กด ๋ฐฉ์์ ํ๊ณ (Bold ์ฃผ์ฅ)
- ํด๊ฒฐ ์์น (emphasis)
- ๊ตฌ์ฒด์ ๊ตฌํ/ํด๊ฒฐ์ฑ
- ๊ตฌ์กฐ/์์ ์ ๋ฆฌ (ํ ๋๋ ํธ๋ฆฌ)
- ๋ง๋ฌด๋ฆฌ ํ๋ ์ ๋
๊ธฐ์ ์ฌํ ๊ธ์ด ์๋ ๊ฒฝ์ฐ (ํธ๋ ๋/๋จํธ): ํ์ฅ ํ
ํ๋ฆฟ ๋์ ๊ธฐ๋ณธ ๊ตฌ์กฐ(๋ฌธ์ ํ๋ ์ด๋ฐ โ Bold ์ฃผ์ฅ โ Bold ๋ง๋ฌด๋ฆฌ)๋ง ์ฌ์ฉ. "What This Guide Builds" ํ
์ด๋ธ์ ์๋ต ๊ฐ๋ฅ.
Image Types in Blog Posts
Blog posts use THREE types of visual content, each with a different production path:
| Type | Production | Cost | When |
|---|
| Mermaid (inline) | ```mermaid code blocks โ Quartz renders to SVG | $0 | Flows, sequences, state machines |
| Excalidraw PNG (exported) | .excalidraw.json โ excalidraw-to-png.js โ .png | $0 | Architecture diagrams, before/after, data flow |
| SVG Cover (inline) | Model writes SVG XML โ saved as .svg | $0 | Hero/banner, article cover image |
SVG is the primary cover/hero image format โ model writes SVG XML directly. Mermaid is for inline flow diagrams. Excalidraw is for complex architecture visuals.
SVG Meme Templates (Mode B only)
For stories with a natural humor angle, create a meme SVG alongside the cover. Memes make technical content more approachable on social/X.
Supported templates:
| Template | Use Case | Structure |
|---|
| Drake Reject/Approve | Before/after comparison | Two panels: red โ (old) โ green โ (new) |
| "This is fine" | Recognizable pain/bug | Burning room, "it's fine" caption |
| Galaxy Brain | Escalating understanding | 4 levels of insight, last one mind-blowing |
| Distracted Boyfriend | Three-way comparison | 3 labeled elements: old โ new โ shiny |
Save as YYYY-MM-DD-slug-meme.svg. Embed optionally: ![[slug-meme.svg|600]]
Pitfall: Don't force a meme where none fits. If the story is serious (incident, security, reflection), skip it. Memes are for "this is ridiculous" or "this pattern is obvious in hindsight" angles only.
โ ๏ธ Pitfall: Do not conflate Mermaid with exported images. Mermaid renders inline as SVG. Excalidraw PNGs are separate files. The blog post needs BOTH โ Mermaid for inline flows, Excalidraw PNG for architecture visuals.
Excalidraw โ PNG Pipeline
The content-manager creates .excalidraw.json files for complex architecture. The full pipeline is:
Step 1: Create JSON โ Validate
Create .excalidraw.json with proper JSON structure. Must validate before proceeding:
python3 -c "import json; json.load(open('diagram.excalidraw.json'))" && echo "VALID" || echo "INVALID"
Common pitfall: trailing comma in the last element of an array or object. JSON does not allow trailing commas. Use write_file which includes a JSON lint step, or run the validation above.
Step 2: Create .excalidraw binary
excalidraw create diagram.excalidraw.json -o diagram.excalidraw
This step converts the JSON into a binary .excalidraw file. This is a required intermediate step โ the PNG script below needs it.
Step 3: Export to URL
excalidraw export diagram.excalidraw
Uploads to excalidraw.com, returns a shareable URL. Requires internet access.
Step 4: Screenshot to PNG
node ~/.{{AGENT_NAME_LOWER}}/scripts/excalidraw-to-png.js \
/path/to/diagram.excalidraw.json \
/path/to/diagram.png
This runs Puppeteer (headless Chrome) to open the URL in ?embed=1 mode and screenshot the canvas.
Setup:
npm install -g excalidraw-cli
cd ~/.{{AGENT_NAME_LOWER}}/scripts
npm install puppeteer
Runtime requirements: Node.js, internet access to excalidraw.com, puppeteer in ~/.{{AGENT_NAME_LOWER}}/scripts/node_modules/.
Shortcut โ the excalidraw-to-png.js script combines Steps 3+4 (export + screenshot). You still need Step 2 (excalidraw create) separately.
Embed in blog post:
![[diagram.png|700]]
*{caption}*
SVG Cover Image (Generated Inline, $0)
The content-manager generates an SVG cover image for each blog post. SVG is XML text โ the model writes it directly. No API calls, no extra cost.
Template (1200ร630, YOUR_DOMAIN dark theme):
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0f0f1a"/>
<stop offset="100%" style="stop-color:#1a1a2e"/>
</linearGradient>
</defs>
<rect width="1200" height="630" fill="url(#bg)"/>
</svg>
Design rules:
- Background:
#0f0f1a โ #1a1a2e gradient
- Accent:
#7b5f3d (amber โ brand color)
- Secondary:
#4a90d9 (blue) or #50c878 (teal)
- Text:
#e8e4df (warm white), #8a8680 (muted)
- Subtle grid:
#ffffff at 3% opacity
- Include: title, subtitle, simple architecture illustration, tags, date
- Embed as:
![[YYYY-MM-DD-slug-cover.svg|800]]
The SVG replaces the old <!-- COVER: ... --> HTML comment pattern (deprecated). No paid API needed โ the model writes SVG markup directly.
Pitfall: Do NOT fall back to paid image APIs (FAL, OpenAI, Gemini Image). The user explicitly rejected these. SVG generation is the designated zero-cost approach. If the SVG is too complex, simplify it โ don't suggest payment.
X Thread Production
Every blog post gets a companion X thread. The thread is a self-contained narrative โ it should be understandable without reading the blog post, while linking back to it.
Thread Structure (12 tweets โ sweet spot for ัะธัะฐัะตะปั retention)
1/HOOK โ Bold claim or surprising question. Make the reader pause.
Example: "\"Is this even working?\" This question found 3 silently broken
bugs. Here's what happened."
2-3/ โ Problem setup. What was supposed to work, what was actually happening.
One tweet per bug or per layer of the problem.
4-7/ โ The discovery process. How you found it, what the root cause was.
Concrete details matter: error codes, line counts, hours stranded.
8-9/ โ The pattern. What these bugs share. The lesson that generalizes beyond
your specific setup.
10-11/โ The fix. What changed. Keep it short โ the detail is in the blog post.
12-13/โ Broader implication + CTA. "This is what taste in engineering looks like."
Link to the blog post.
Tweet Content Rules
- Each tweet is self-contained. Don't assume the reader saw the previous tweet (but do assume Twitter threading renders them in order).
- Korean only (following brand guide). English terms allowed for proper nouns (PYTHONPATH, GraphQL, etc.)
- Use emoji sparingly โ ๐งต at the end of tweet 1, that's it. Bug emoji (๐) OK for bug posts.
- One tweet = one idea. Don't cram 3 bugs into one tweet.
- Link in last tweet only (or penultimate). Earlier tweets that link cannibalize engagement.
- No hashtags in tweet body (they look spammy). Hashtags in last tweet only if at all.
Thread Ending
Last tweet: Season/Episode tag (e.g., "Season 1: Taste Engineering โ Episode 3") + link.
X Thread File Convention
Save as YYYY-MM-DD-slug-thread.txt in memories/insights/. Plain text, one tweet per paragraph separated by blank lines. The numbered "N/15" prefix is added by the human on posting.
Hooked question
2/ Bug description
3/ Pattern discovery
...
Last/ Link + season tag
Pitfall: Don't write the "N/15" counting in the file โ the human decides the final tweet count. Write the content; the /N comes from the posting interface.
Cover images are <!-- COVER: description --> HTML comments โ no image generation. The content-manager describes the ideal image; a human or future tool generates it.
This pattern is deprecated in favor of SVG cover generation above. Keep only if SVG generation fails (unlikely โ SVG is just XML text).
์ด๋ฏธ์ง ๋ฐฐ์น ์์น โ "์ค๋ช
์ ์ ๋ณด์ฌ์ฃผ๊ธฐ":
- ์น์
๋ด์์ ํต์ฌ ๊ฐ๋
์ ๋จผ์ ์ค๋ช
ํ๋ฉด ๋
์๊ฐ ๊ทธ๋ฆผ์ ๋ณด๋ฉด์ ๋ด์ฉ์ ์์ฐ์ค๋ฝ๊ฒ ์ดํดํจ
- ๋ค์ด์ด๊ทธ๋จ โ ์ค๋ช
์์: "![[flow-diagram.png]]" โ "์ ๊ทธ๋ฆผ์ X์ ๊ตฌ์กฐ๋ฅผ ๋ณด์ฌ์ค๋ค"
- ์คํฌ๋ฆฐ์ท โ ์ค๋ช
์์: "![[workspace-screenshot.png]]" โ "์ค์ ์ํฌ์คํ์ด์ค๋ ์ด๋ ๋ค"
- ๋น๊ต ์ด๋ฏธ์ง โ ์ค๋ช
์์: "![[before-after.png]]" โ "๋ฐ๊พธ๊ธฐ ์ /ํ ์ฐจ์ด๋ค"
๊ธฐ์ ์ฌํ ๊ธ์ ์ด๋ฏธ์ง ์ ๋ต:
- 1๊ฐ ํ์ โ ์ปค๋ฒ ์ด๋ฏธ์ง ๋๋ ์ฃผ์ ๋ค์ด์ด๊ทธ๋จ (๊ธ ๋์
๋ถ ๊ทผ์ฒ)
- 1~2๊ฐ ์ ํ โ ์ํคํ
์ฒ ํ๋ก์ฐ, ๋น๊ต ์๊ฐํ, UI ๋ชจ์ ๋ฑ
- ๋๋น ์กฐ์ โ Obsidian embed์
|300 |400 |600์ผ๋ก ๋ณธ๋ฌธ ํญ์ ๋ง์ถค
- ๋๋ฌด ๋ง์ง ์๊ธฐ โ 3๊ฐ ์ด์์ด๋ฉด ์ง์ค๋ ฅ ๋ถ์ฐ, 1~2๊ฐ๊ฐ ์ด์์
์์ โ ํธ๋ ๋ ๊ธ:
# AI ์์ด์ ํธ, ์ฆ๊ฑฐ ์์ด ๋ตํ์ง ๋ง๋ผ
**๋ฌธ์ ํ๋ ์ด๋ฐ**
๋น์ ์ด AI ์์ด์ ํธ๋ฅผ ๊ตฌ์ถํ๋ค๊ณ ์น์. ๋ชจ๋ํฐ๋ง์ PagerDuty์์ ํ๊ณ , ์๋ฌ๋ Sentry์ ์๊ณ , ๋ฐฐํฌ๋ GitHub Actions์์ ๋๋ค. ๋ฌธ์ ๋ ์ฌ๋ฌ ๊ณณ์ ํฉ์ด์ ธ ์๋๋ฐ, ์ผ๋ฐ ์ฑ๋ด์ "Sentry์์ ์๋ฌ๋ฅผ ํ์ธํด๋ณด์ธ์"๋ผ๊ณ ๋ง ๋งํ ์ ์๋ค.
**๊ฐํ ์ฃผ์ฅ**
ํ์ง๋ง ๊ทธ๊ฒ์ triage๊ฐ ์๋๋ค. ๊ทธ๊ฑด ์ ๋๋ ํ ์ผ ๋ชฉ๋ก์ผ ๋ฟ์ด๋ค.
**๋์์ธ ์ ์ฝ**
์์น์ ๋จ์ํ๋ค: ์ฆ๊ฑฐ ์์ด๋ ๋ตํ์ง ์๋๋ค.
**ํด๊ฒฐ์ฑ
**
์ด ์์น์ ์งํค๊ธฐ ์ํด Coral์ ๋ฐ์ดํฐ ํ๋ ์ธ์ผ๋ก ์ฌ์ฉํ๋ฉด...
4-4. Language Polish โ DeepSeek ํ๊ธ ์ค๋ฌธ (Aggregator Mode Only)
AI ํฐ ์ ๊ฑฐ + ํ๊ธ ๊ต์ ์ DeepSeek๋ก ์ค๋ฌธ. CMO Agent mode์์๋ skip (content-manager๊ฐ ์ง์ ์์ฑ).
์ค๋ฌธ ์ฒ ํ โ ์ญํ ํ์ธ, ์ญ์ ๊ท์น์ด ์๋๋ค.
- "์ด ํํ์ ์ง์์ผ ํ๋?" โ "์ด ํํ์ด ์ฌ๊ธฐ์ ๋ฌด์จ ์ผ์ ํ๋?"๋ก ๋ฐ๊ฟ์ ํ๋จ
-์ , ์, ๋ค, ๊ฒ, ์๋/์๋ค๋, ์์๋ค ๋ฑ์ ํจํด ๋งค์นญ์ผ๋ก ๋ฌด์กฐ๊ฑด ์ญ์ ํ์ง ์๊ณ , ์๋ฏธยท๋ฆฌ๋ฌยท๊ฐ์กฐยท์ฅ๋ฅด์ ํ์ํ ์ญํ ์ ํ๋์ง ํ์ธ
- ๊ฐ๋
์ดยท์ ๋ฌธ์ฉ์ดยท๋์กฐยท์์ ยท์ถ์ฒยท์ค์ ๋ณต์ยท๊ณ ์ ํํ์ด๋ฉด ์ ์ง
๋ณ๊ฒฝ๋ฅ ๊ฐ๋:
- ๋ชฉํ: 5~30%. 30% ์ด๊ณผ ์ ๊ณผ์ค๋ฌธ ๊ฐ๋ฅ์ฑ ์ฌ๊ฒํ
- 50% ์ด๊ณผ ์ ๊ฐ์ ์ค๋จ, ์๋ฌธ ์ ์ง, summary์ "๋ณ๊ฒฝ๋ฅ 50%+ โ ์ฌ๋ ๊ฒํ ๊ถ๊ณ " ํ๊ธฐ
Vault์์ API ํค ์กฐํ:
import sys, os
sys.path.insert(0, os.path.expanduser("~/.{{AGENT_NAME_LOWER}}"))
from modules.secrets_vault import SecretsVault
vault = SecretsVault()
api_key = vault.resolve("vault_9fa1b5bb")
์คํ:
DEEPSEEK_API_KEY="<vault์์ ์กฐํํ ํค>" \
python3 ~/.{{AGENT_NAME_LOWER}}/P4-cortex/scripts/humanize_korean.py \
~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{YYYY-MM}-{slug}.md \
~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{YYYY-MM}-{slug}_polished.md
์คํฌ๋ฆฝํธ: ~/.{{AGENT_NAME_LOWER}}/P4-cortex/scripts/humanize_korean.py
- DeepSeek API (deepseek-chat) ํธ์ถ
- AI ํจํด ์ ๊ฑฐ: ~์ ๋ํด, ~๋ผ๊ณ ์๊ฐํ๋ค, ์
๋๋ค/์ต๋๋ค ending ๊ณผ์, "//" ์คํ์ผ
- Markdown formatting ๋ณด์กด
- ์๋ณธ ์์ ์์ (๋ณ๋ ํ์ผ๋ก ์ถ๋ ฅ)
- temperature=0.3 (์์ฐ์ค๋ฌ์ด variation, ์ฌ์ค ์๊ณก ๋ฐฉ์ง)
์คํ ํ:
_polished.md ํ์ผ์ด ์์ฑ๋จ โ ์๋ณธ๊ณผ ๊ต์ฒด (rename)
- ์ดํ forbidden.patterns grep ์คํ
Vault ref:
| ์๋น์ค | ref | ํ์ธ |
|---|
| DeepSeek API | vault_9fa1b5bb | vault.list("api_key")๋ก ํ์ธ |
Vault์ ํค ๋ฑ๋ก (์ต์ด 1ํ):
from modules.secrets_vault import vault
vault.register("DEEPSEEK_API_KEY", os.getenv("DEEPSEEK_API_KEY", "sk-your-key-here"), category="api_key")
4-5. Language Polish ์ฒดํฌ๋ฆฌ์คํธ (DeepSeek ์ค๋ฌธ ํ)
DeepSeek ์ค๋ฌธ์ ์ฌ์ฉํ ๊ฒฝ์ฐ ์ด ๋จ๊ณ๋ skip โ DeepSeek๊ฐ ์ฒ๋ฆฌํจ.
Polish ์ฒดํฌ๋ฆฌ์คํธ (DeepSeek ์ค๋ฌธ ํ):
[ ] humanize_korean.py ์คํ ์๋ฃ
[ ] _polished.md โ ์๋ณธ ํ์ผ๋ก rename
[ ] forbidden.patterns grep โ 0๊ฑด
[ ] ๋ณ๊ฒฝ๋ฅ 5~30% (30%+ = ๊ณผ์ค๋ฌธ ๊ฒํ , 50%+ = ์ค๋จ)
[ ] Sunny ์ญํ ํ์ธ: ํํ์ด ๋ฌด์จ ์ผ์ ํ๋๊ฐ? (์๋ฏธยท๋ฆฌ๋ฌยท๊ฐ์กฐยท์ ๋ฐ๋ยท์ฅ๋ฅด)
์๋ฌธ ์์ (์์ ์ ):
์ด ๅ่ฝ์ ๆจ็ ะถะธะทะฝั์ ๋งค์ฐ ้่ฆํฉ๋๋คไธๅฏไปฅ
์์ ํ (DeepSeek):
์ด ๊ธฐ๋ฅ์ ๋น์ ์ ์ถ์ ๋งค์ฐ ์ค์ํฉ๋๋ค
์คํจ ์: ์๋ณธ draft ํ์ผ ๊ทธ๋๋ก ์ ์ง (์์
์คํจ๊ฐ ์๋). Phase 5๋ก ์งํ.
4-6. Kanban Complete (Aggregator Mode Only)
draft_file = f"~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{YYYY-MM}-{slug}.md"
kanban_complete(
task_id,
result=f"Draft file: {draft_file}",
summary=f"Draft created: {title}",
metadata={"draft_path": f"P2-hippocampus/memories/insights/{YYYY-MM}-{slug}.md"}
)
result์๋ ๋ฐ๋์ ์ ๋๊ฒฝ๋ก ํฌํจ โ Phase 5 Periodic Delivery์ SQLite regex๊ฐ ~ ๋๋ ~/๋ก ์์ํ๋ ๊ฒฝ๋ก๋ฅผ ์ถ์ถํจ.
Phase 5 regex: r'memories/insights/(20\d{2}-\d{2}-[^.]+\.md)' ๋๋ r'~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/(20\d{2}-\d{2}-[^.]+\.md)'
4-7. Post-production: narrative_arc + content-inventory Sync
After every blog post + X thread, update BOTH tracking files before completing the cycle.
narrative_arc.md (P4-cortex/content/narrative_arc.md) โ Two updates:
- Episodes table: add new row
- Decision log: add entry explaining why
content-inventory.md (P4-cortex/content/content-inventory.md) โ Three updates:
- Published table: add post row
- Drafts table: add all supporting files
- Topics Covered: add 1-2 dedup bullets
Pitfall: Do not list the same draft file in both Published and Drafts tables.
Phase 5: User Review
Review Steps (YOUR_USERNAME)
- Obsidian์์
memories/insights/ ํ์ธ
- draft ํ์ผ ์ด๊ธฐ
- Editorial Gate ์ฌํ์ธ:
- Drew-angle์ด ์ค์ ๋ก ๋๋ฌ๋๋๊ฐ?
- ๋
์๊ฐ ๊ฐ์ ธ๊ฐ ํ๋จ ๊ธฐ์ค์ด ์๋๊ฐ?
- ๋จ์ ํธ๋ ๋ ์์ฝ์ด ์๋๋ผ ํด์์ด ์๋๊ฐ?
- 6๊ฐ์ ๋ค์๋ portfolio/insight ๊ธฐ๋ก์ผ๋ก ์๋ฏธ๊ฐ ์๋๊ฐ?
- ์คํ์ยท๋ด์ฉ ์์
- frontmatter ํ์ธ:
- ํต๊ณผ:
status: draft โ status: published
- ๋ณด๋ฅ:
status: draft โ status: in_review
- ํ๊ธฐ:
status: draft โ status: archived
publish_date: {YYYY-MM-DD} ์ถ๊ฐ
- Quartz rebuild (์๋ ๋๋ ์๋)
Quality Gate (writer์ฉ)
Draft ์์ฑ ์๋ฃ ์ ํ์ธ. ๋ชจ๋ ํจํด ๋งค์นญ์ "์ญ์ ๊ท์น์ด ์๋๋ผ ์ญํ ํ์ธ" โ ํํ์ด ์ฌ๊ธฐ์ ๋ฌด์จ ์ผ์ ํ๋์ง ๋จผ์ ํ๋จํ๊ณ ์์ ํ ๊ฒ.
- Language Polish (4-4 ๋จ๊ณ) ๋จผ์ ์ํ โ ํ์/ํ๋ฌธ/์ผ์ดโํ๊ธ ๋ณํ, ๋น๋ฌธ ์์
- forbidden.patterns grep โ 0๊ฑด
- Bold ์น์
๊ฐ์กฐ 2~4๊ฐ ์กด์ฌ
- "๋น์ " ์ง์ ํธ์นญ + 1์ธ์นญ "์ "/"๋" ํฌํจ
- ๋ณธ๋ฌธ ๋ ์ง ์์ (X์ X์ผ ๊ธ์ง)
- ์ถ์ฒ ์๋ ์ฃผ์ฅ ์์ (๋ชจ๋ฅด๋ฉด "ํ์ธ ํ์" ํ๊ธฐ)
- SEO ํค์๋ 3
7๊ฐ, ํด์ํ๊ทธ 814๊ฐ
- aliases in frontmatter (
/blog/{slug})
- Editorial Gate: Drew-angle + reusable insight + specificity ํต๊ณผ
Polish ์ฒดํฌ๋ฆฌ์คํธ ์ถ๋ ฅ ์์:
Language Polish: DONE
ํ์โํ๊ธ: 3๊ฑด ๋ณํ
ํ๋ฌธ ํํ: 1๊ฑด ๋ณํ
์ผ์ด ์ธ๋์ด: 0๊ฑด
๋น๋ฌธ ์์ : 2๊ฑด
Forbidden: 0๊ฑด
Bold sections: 3
Direct address ("๋น์ "): YES
1์ธ์นญ ("์ "/"๋"): YES
Dates in body: 0
SEO keywords: 5
ํด์ํ๊ทธ: 11
aliases: /blog/gemini-cli-shutdown
PASS
## Content Pipeline โ YYYY-MM-DD HH:MM KST
Topics selected: N
| # | Source | Topic | Task ID | Draft File |
|---|--------|-------|---------|------------|
| 1 | trend | {title} | {id} | ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{YYYY-MM}-{slug}.md |
| 2 | seo | {title} | {id} | ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{YYYY-MM}-{slug}.md |
| 3 | conversation | {title} | {id} | โ (in progress) |
Worker ๋ฐฐ๋ถ: kanban-dispatcher-content๊ฐ 5๋ถ๋ง๋ค ready task๋ฅผ worker์ ๋ฐฐ๋ถ
Review at: Obsidian โ P2-hippocampus โ memories โ insights
Phase 3 ์๋ฃ Delivery (Phase 3 ํ ์ฆ์ ์ถ๋ ฅ)
## Content Pipeline โ YYYY-MM-DD HH:MM KST
Topics selected: N (task created)
| # | Source | Topic | Task ID | Draft File |
|---|--------|-------|---------|------------|
| 1 | trend | {title} | {id} | ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{YYYY-MM}-{slug}.md |
| 2 | seo | {title} | {id} | ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{YYYY-MM}-{slug}.md |
| 3 | conversation | {title} | {id} | โ (in progress) |
Worker ๋ฐฐ๋ถ: kanban-dispatcher-content๊ฐ 5๋ถ๋ง๋ค ready task๋ฅผ worker์ ๋ฐฐ๋ถ
Review at: Obsidian โ P2-hippocampus โ memories โ insights
Topics selected=0์ด๋ฉด [SILENT].
Phase 3 Periodic Delivery (๋งค cron tick๋ง๋ค, ์๋ฃ๋ task ํ์ธ)
content-pipeline cron job์ด ๋งค ์คํ๋ง๋ค content board์ completed task๋ฅผ ์กฐํํด draft ํ์ผ ์์น๋ฅผ ๋ณด๊ณ :
import sqlite3, os, re, pathlib, glob
DB = os.path.expanduser("~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/kanban/state/{{AGENT_NAME_LOWER}}_tasks.db")
INSIGHTS = os.path.expanduser("~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/")
conn = sqlite3.connect(DB)
rows = conn.execute("""
SELECT id, title, body, completed_at
FROM tasks
WHERE trigger_source = 'content_pipeline'
AND board = 'content'
AND status = 'completed'
AND completed_at > datetime('now', '-72 hours')
ORDER BY completed_at DESC
""").fetchall()
conn.close()
drafts = []
for task_id, title, body, completed_at in rows:
draft_path = "โ (path not recorded)"
if body:
m = re.search(r'P2-hippocampus/memories/insights/(\d{4}-\d{2}-[^/]+\.md)', body)
if m:
draft_path = os.path.join(INSIGHTS, m.group(1))
if not os.path.exists(draft_path):
date_prefix = m.group(1)[:7]
matches = glob.glob(os.path.join(INSIGHTS, f"{date_prefix}-*.md"))
draft_path = max(matches, key=os.path.getmtime) if matches else f"~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{m.group(1)}"
drafts.append((title, task_id, draft_path))
์๋ฃ๋ task๊ฐ ์์ผ๋ฉด่ฟไปฝ delivery ์ถ๋ ฅ:
## Content Pipeline Update โ YYYY-MM-DD HH:MM KST
Draft files ready for review:
| # | Topic | Task ID | Draft File |
|---|-------|---------|------------|
| 1 | {title} | {id} | ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{filename}.md |
| 2 | {title} | {id} | ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{filename}.md |
Review at: Obsidian โ P2-hippocampus โ memories โ insights
์๋ฃ๋ task๊ฐ ์์ผ๋ฉด (completed_at์ด 72์๊ฐ ๋ด ์์ผ๋ฉด) [SILENT].
## Content Pipeline โ YYYY-MM-DD HH:MM KST
Topics selected: N
| # | Source | Topic | Task ID | Draft File |
|---|--------|-------|---------|------------|
| 1 | trend | {title} | {task_id} | ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{filename}.md |
| 2 | seo | {title} | {task_id} | ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/{filename}.md |
| 3 | conversation | {title} | {task_id} | โ (in progress) |
Review at: Obsidian โ P2-hippocampus โ memories โ insights
Draft File ๅ์ task body์์ P2-hippocampus/memories/insights/ ๊ฒฝ๋ก๋ฅผ ์ถ์ถ โ ์์ผ๋ฉด ์ ๋๊ฒฝ๋ก๋ก ํ์, ์์ผ๋ฉด "โ (in progress)" ํ์.
Phase 6: Publishing (Quartz Auto-Deploy โ No Git)
Publishing Flow
Obsidian์์ status: published๋ก ๋ฐ๊พธ๋ฉด โ fswatch๊ฐ vault ๋ณ๊ฒฝ ๊ฐ์ง โ quartz build โ wrangler pages deploy โ YOUR_DOMAIN ๊ฒ์ (์ฝ 10์ด ๋ด์ธ)
YOUR_USERNAME (Obsidian์์ draft ํธ์ง)
โ frontmatter: status: published, publish_date: YYYY-MM-DD
โ fswatch๊ฐ vault ํ์ผ ๋ณ๊ฒฝ ๊ฐ์ง (LaunchAgent ์๋ ์คํ)
โ 5์ด debounce ํ quartz build
โ wrangler pages deploy public/ --project-name=YOUR_SITE
โ Cloudflare Pages์ directory deploy (git ๋ถํ์)
โ YOUR_DOMAIN ๊ฒ์
wrangler pages deploy๋ git ์์ด directory๋ฅผ ์ง์ Cloudflare Pages์ ์ฌ๋ฆฝ๋๋ค.
git repo ์๋์ด๋ ๋์, git commit ๋ถํ์.
Infrastructure
| Component | Details |
|---|
| fswatch LaunchAgent | com.{{AGENT_NAME_LOWER}}.quartz-fswatch (PID 5247 โ
) |
| fswatch script | ~/.{{AGENT_NAME_LOWER}}/P4-cortex/scripts/quartz-fswatch.sh |
| Watched dirs | memories/insights, P4-cortex/growth, P4-cortex/knowledge, YOUR_SITE/content |
| Debounce | 5์ด (๋ณ๊ฒฝ ํ 5์ดๅ
ๆ ์ถ๊ฐ ๋ณ๊ฒฝ์ด๋ฉด ๋ค์ 5์ด ๋๊ธฐ) |
| Build | cd YOUR_SITE && npx quartz build --concurrency=4 |
| Deploy | wrangler pages deploy public/ --project-name=YOUR_SITE (git ๋ถํ์) |
| Deploy LaunchAgent | com.{{AGENT_NAME_LOWER}}.quartz-deploy (runs on-demand via wrapper script) |
| CF Account ID | dc0199b6b6c27bc9bb2f3201d47cb643 |
| CF Project | YOUR_SITE |
| Site URL | https://YOUR_DOMAIN |
LaunchAgent States
fswatch:
5247 running com.{{AGENT_NAME_LOWER}}.quartz-fswatch โ vault ๋ณ๊ฒฝ ๊ฐ์ง
63582 running com.{{AGENT_NAME_LOWER}}.quartz-deploy โ (KeepAlive, ํ์์ ์คํ)
YOUR_DOMAIN ์ค์๊ฐ ๊ฒ์ ์ํ:
vault ํ์ผ ๋ณ๊ฒฝ โ fswatch ๊ฐ์ง โ 5์ด debounce โ quartz build โ wrangler deploy โ ~3์ด ํ ๊ฒ์
Managing LaunchAgents
launchctl list | grep quartz
launchctl unload ~/Library/LaunchAgents/com.{{AGENT_NAME_LOWER}}.quartz-fswatch.plist
launchctl load -w ~/Library/LaunchAgents/com.{{AGENT_NAME_LOWER}}.quartz-fswatch.plist
tail -f ~/Library/Logs/quartz-fswatch.log
tail -f ~/Library/Logs/quartz-deploy.log
Content โ Site Mapping
Content-manager drafts live at ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/. The Quartz site maps this directory into its content tree via symlink:
YOUR_SITE/content/insights โ P2-hippocampus/memories/insights/
โ ๏ธ Pitfall: The symlink was previously pointing to P4-cortex/knowledge (wrong dir). If drafts aren't appearing in the build, check:
readlink ~/.{{AGENT_NAME_LOWER}}/YOUR_SITE/content/insights
Deploy Loop Prevention
The fswatch โ quartz build โ wrangler deploy pipeline can enter an infinite loop if build output triggers another fswatch event. Symptoms: deploy log repeating every 10 seconds, launch agents showing error exit codes.
Fix: The deploy script should check if a build is already running before starting a new one. Use a lock file:
LOCKFILE=/tmp/quartz-deploy.lock
if [ -f "$LOCKFILE" ] && [ $(($(date +%s) - $(cat "$LOCKFILE"))) -lt 60 ]; then
log "Deploy already running, skipping"
exit 0
fi
date +%s > "$LOCKFILE"
rm -f "$LOCKFILE"
[ ] ์คํ์ยท๋ด์ฉ ์์
[ ] frontmatter: status: published (draft โ published)
[ ] frontmatter: publish_date: YYYY-MM-DD ์ถ๊ฐ
[ ] aliases: ['/blog/YYYY/slug'] ๋๋ ['/blog/slug'] ์ค์ (SEO-friendly URL)
[ ] fswatch๊ฐ ์๋ ๊ฐ์ง โ YOUR_DOMAIN ๊ฒ์ (~10์ด)
[ ] YOUR_DOMAIN์์ ๊ฒ์ ํ์ธ
Manual Trigger (LaunchAgent ์์ด)
bash ~/.{{AGENT_NAME_LOWER}}/P4-cortex/scripts/quartz-deploy.sh
cd ~/.{{AGENT_NAME_LOWER}}/YOUR_SITE
npx quartz build
wrangler pages deploy public/ --project-name=YOUR_SITE
Configuration Files
~/.{{AGENT_NAME_LOWER}}/YOUR_SITE/
โโโ wrangler.toml โ project_name = "YOUR_SITE"
โโโ .wrangler.jsonv2 โ account_id, CF Pages project ์ค์
~/.{{AGENT_NAME_LOWER}}/P4-cortex/scripts/
โโโ quartz-deploy.sh โ build + wrangler deploy ์คํฌ๋ฆฝํธ
โโโ quartz-fswatch.sh โ fswatch ํ์ผ ๋ณ๊ฒฝ ๊ฐ์ง โ debounce โ deploy
Diagrams & Images in Blog Posts
Mermaid Diagrams (Inline, Auto-Rendered by Quartz)
Quartz at YOUR_DOMAIN has Mermaid enabled (quartz/components/scripts/mermaid.inline.ts). Write ````mermaid code blocks directly in blog post markdown:
```mermaid
graph TD
subgraph "Source"
A[Git] --> D
B[Kanban] --> D
end
D{Agent} --> E[Blog Draft]
D --> F[X Thread]
Common types: `graph TD` (top-down), `graph LR` (left-right), `sequenceDiagram`, `flowchart TD`.
### Excalidraw Diagrams (Hand-Drawn Architecture Style)
For complex architecture/flow diagrams, create an `.excalidraw.json` companion file. Follows the visual style from the ReefWatch article (dev.to/siiddhantt/building-reefwatch).
The blog post references it as: `<!-- EXCALIDRAW: slug.excalidraw.json -- architecture diagram -->`
The JSON opens in Obsidian (Excalidraw plugin) or excalidraw.com for PNG export.
### Cover Image Descriptions
Add an HTML comment describing the ideal cover image:
```markdown
<!-- COVER: Dark mode dev dashboard showing architecture, amber accents, YOUR_DOMAIN style. -->
Placeholder โ image generated later by image_gen tool or human designer.
Obsidian Publishing Workflow
Step-by-Step
- Draft ์์ฑ (Phase 4) โ memories/insights/์draft ํ์ผ ์์ฑ
- Obsidian์์ ํธ์ง โ ์คํ์ยทๅ
ๅฎนใฎ๊ฒฌยท๊ธ์ฐ๊ธฐ ๊ต์
- Publication ์ค๋น โ frontmatter์์:
status: published
publish_date: 2026-05-27
- fswatch๊ฐ ์๋ ๊ฐ์ง โ vault ํ์ผ ๋ณ๊ฒฝ โ quartz build โ Cloudflare Pages ๋ฐฐํฌ
- YOUR_DOMAIN์์ ํ์ธ โ 1~2๋ถ ๋ด ๊ฒ์
Alias ๊ท์น (SEO-Friendly URL)
| Frontmatter | Site URL |
|---|
aliases: ['/blog/2026/agent-workflow'] | YOUR_DOMAIN/blog/2026/agent-workflow |
aliases: ['/projects/kanban-review'] | YOUR_DOMAIN/projects/kanban-review |
aliases: ['/lab/dream-system'] | YOUR_DOMAIN/lab/dream-system |
Slug ์์ฑ ๊ท์น:
- Timestamp ์ ๊ฑฐ:
KANBAN-REVIEW-20260520 โ kanban-review
- ์๋ฌธ์ + kebab:
Open Crab Ontology โ open-crab-ontology
- ์๋ฏธ ์๋ ๋จ์ด ์ ๊ฑฐ:
The, A, An
WordPress Publishing (Mode B)
Drafts can be pushed to the YOUR_DOMAIN WordPress site via the WordPress MCP server.
Review & Approve Workflow
content-manager draft โ memories/insights/ ์ ์ฅ
โ (future) Huly issue ์์ฑ (status: Todo)
โ Drew reviews in Huly โ Done
โ publisher cron โ WordPress MCP push
See references/wordpress-publish-workflow.md for the full implementation guide.
Related
- [[skills/wordpress-cms]] โ WordPress Docker + Blocksy + MCP setup
- [[writing-style-guide]] โ Writing tone & rules
- [[@memory/kanban/KANBAN_INDEX]] โ Kanban board integration
- [[skills/kanban-worker]] โ Worker execution model
- [[skills/content-writer]] โ Draft writing skill (๋ณ๋)
โ ๏ธ Path Pitfalls (all modes)
Draft paths in agent profiles, cron prompts, and workflow documents must be absolute paths (~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/). Do NOT use:
- Relative paths like
memories/insights/ โ the agent's cwd may not resolve correctly
~ expansion like ~/.{{AGENT_NAME_LOWER}}/... โ some contexts (cron, subagent spawned by dispatcher) don't expand tilde
โ
Safe: ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/filename.md
โ Unsafe: memories/insights/filename.md
โ Unsafe: ~/.{{AGENT_NAME_LOWER}}/P2-hippocampus/memories/insights/filename.md