Imported from refluster/ai-native-article (
workforce/skills/article-level2/SKILL.md). Install upstream withnpx skills add refluster/ai-native-article --skill article-level2. Copyright stays with the author.
name: article-level2
description: Convert one L1 source entry into one L2 explanation (briefing-document) article, written in Japanese and then issued in both Japanese and English — the agent-workforce equivalent of the GAS L1→L2 batch. Use when an editorial-stream agent must turn an uncovered L1 source into a faithful, evidence-grounded explanation: an Executive Summary up front, source-specific section headings, and every number/name/date/quote taken verbatim from the source. Published to Notion as Type=explanation, Author={agent_slug}, Status=ready, with the English edition on an EN child page.
article-level2
Convert one L1 source entry into one L2 explanation article in Japanese.
This is the agent-workforce counterpart of the GAS L2_BATCH / handleL2Create
pipeline (newsletter/gas/src/Code.gs). The GAS path fetches a source URL, builds a
briefing-document prompt, calls Azure, and writes an explanation-type row into
the unified Notion Articles DB. This skill produces the same deliverable — a
faithful briefing-document explanation — attributed to the running agent so the
article carries a byline on kohuehara.xyz.
It runs on the CCR execution model (the same pattern as Dario's feed-post):
the binding is executor=claude-code-routine + scheduler=external/api, fired
every 2 hours by wf-orchestrator-tick into the generic agent-runner routine
(workforce/docs/routines/agent-runner.md). The routine composes your persona +
this skill body, you generate the explanation, then a bundled write script
owns the Notion write — you do not hand-edit any file and do not open a PR.
Two Notion DBs, one credential (apiKey only)
This skill reads the L1 source library and writes the unified Articles DB —
two different databases. Only the Notion apiKey is a secret; the two database
ids are not secret (they're already committed in newsletter/gas/src/Code.gs and
newsletter/pipeline/normalize-categories.mjs), so the scripts hold them as constants. You
therefore need just one injected credential:
| Credential | Shape | Used for |
|---|---|---|
notion.integration_token |
{apiKey, …} — only apiKey is read |
both pick-l1-source.mjs (read L1 + unified coverage) and publish-notion.mjs (write the explanation to the unified DB) |
The Notion integration behind apiKey must be shared with both databases in
Notion. (L2 and L3 share the unified Articles DB, distinguished by Type; a future
article-level3 skill reuses the same credential + unified DB. L1 is the one
genuinely separate DB — its id is a constant in pick-l1-source.mjs.)
Instructions
-
Pick one uncovered L1 source — run the picker, don't guess. Run
pick-l1-source.mjs(below). It queries L1 + the unified DB, walks the uncovered rows oldest-first, and returns the first one whose source body it could actually fetch — writing that body tobodyFilefor you. Rows it could not fetch have their failure recorded on the L1 row and are stepped over, so a bot-walled URL no longer stalls the queue.If it returns
{"skip": true, …}, stop — produce nothing this fire. Otherwise your subject is the returned{title, summary, sourceUrl}and your evidence is the contents ofbodyFile.NOTION_API_KEY="<credentials['notion.integration_token'].apiKey>" \ node workforce/skills/article-level2/pick-l1-source.mjs{"l1PageId":"…","title":"…","summary":"…","sourceUrl":"https://…", "category":"…","bodyFile":"/tmp/l2-source-<id>.md","bodyChars":20273,"fetchedVia":"direct"}Read
bodyFileand ground every claim in it. You do not need to fetchsourceUrlyourself — the picker already did, through a direct fetch with a reader fallback, and it only returned this row because the result was substantial enough to ground an article (bodyCharstells you how much). -
Produce one Japanese briefing-document explanation (target ~3000 字).
-
Follow the L2 briefing format (identical to the GAS
buildL2Promptcontract):- Line 1: a
#H1 — a concrete Japanese title specific to the source's subject. No generic placeholders ("AIの可能性", "変化への対応" are banned). - Directly below: a
## Executive Summarysection — a 2–3 sentence lead surfacing the single most important takeaway before the reader scrolls. - Body: 2–5
##sections, each named after a source-specific theme. Separate evidence (facts from the source) from conclusion (the implication you draw). Use short paragraphs plus bullet lists carrying the actual numbers, names, dates, and direct quotations.
- Line 1: a
-
Do not append a bias-disclosure footer (or any byline boilerplate) to the body. Disclosure is carried by the
Authormetadata — rendered as the AuthorChip byline onkohuehara.xyz— the same policyfeed-postalready follows (Epic-011 §7 / Q9). In-body boilerplate duplicates that metadata, freezes a model id in prose, and was the trigger for the ML-006 deploy-gate false positive. -
Then issue the same explanation in English (ADR-0005). See the next section — this is one more output, not one more article.
Two editions, one act of judgment (ADR-0005)
Every article ships in Japanese and English. This does not double the work and must not double the thinking:
- Everything upstream of the writing is shared. One pick, one source body, one reading of the evidence, one set of tags, one set of conclusions. Do not re-fetch the source for the English edition; do not re-decide what the article argues; do not let the two editions reach different conclusions or carry different numbers. If they disagree about a fact, one of them is wrong.
- Only the rendering differs. Write the Japanese explanation first — it is the article. Then write the English edition of that article: same structure, same section order, same evidence, same figures, quotations restored to their original English wording where the source was English.
- English is an edition, not a gloss. Write it as an English-language publication would: natural English prose, not transliterated Japanese sentence order. Keep the headline concrete and specific in the same way the Japanese one is. Japanese terms with no clean English equivalent keep the original with a short parenthetical.
- The same hard rules apply to both. No invented facts, no rounded figures, no reviewer-voice hedges, no bias-disclosure footer.
Format for the English edition, identical in shape to the Japanese one:
- Line 1: a
#H1 — the English title. - Directly below: a
## Executive Summarysection. - Body: the same
##sections in the same order.
Hard rules (editorial integrity — C-1, fail loud — C-4)
- Never invent facts. Every concrete figure, proper noun, date, or quotation must come verbatim from the supplied source. Do not abstract, round, or alter them.
- Source fetching is not your job — it already happened.
pick-l1-source.mjsfetches directly, falls back to the reader (https://r.jina.ai/<url>) when the direct body is thin, retries a throttled reader, and only hands you a row whose body cleared the groundability threshold. The host-by-host routing rule that used to live here (x.com / linkedin / nytimes / ft / wsj / bloomberg / mckinsey → reader) has been deleted on purpose: it was a snapshot of which hosts were walled when it was written, and it decayed in both directions — the reader now 403s for x.com, while reuters.com was walled and was never on the list.scripts/lib/source-fetch.mjsdecides by result, not by hostname. - Never write around a missing body. If you did get a pick,
bodyFilehas real text in it — use it. Do not pad fromsummary(L1 rows registered by the capture endpoint have an empty summary by design) and never substitute your own knowledge of the topic for what the source says. - If you cannot link to or quote the source, do not publish — escalate.
- Objective, incisive tone. Avoid reviewer-voice hedges ("重要だ", "今後注目される") and throat-clearing preambles.
Write the article — run the script, do NOT hand-edit any file
The page is written by a deterministic script, not by you editing JSON. You
generate the judgment (the briefing-document markdown); publish-notion.mjs
owns the write (correct schema, properties, block conversion) by POSTing a new
page into the unified Articles DB with the injected integration token.
Steps:
-
Write the full explanation markdown to a temp file (e.g.
/tmp/l2-article.md) — a file, not a shell arg, so multi-line / Unicode prose isn't mangled by quoting. The first line must be the# TitleH1 (used as the page Title and stripped from the body blocks). Write the English edition the same way to/tmp/l2-article.en.md, also starting with its own# TitleH1. -
Write the abstract — a faithful 2–3 sentence lead (your
## Executive Summary, not the speculative L1 summary) — to a second temp file (e.g./tmp/l2-abstract.txt). This populates theAbstractcolumn, matching how other rows lead. Write the English lead to/tmp/l2-abstract.en.txt. -
Choose 3–5 tags from the controlled flat vocabulary (ADR-0003 /
scripts/lib/tags.mjs) — pick by what the explanation genuinely covers, by topic, not by the L1 source's old letter. The vocabulary:AI Productivity·Agentic AI·Verification & Trust·Engineering Process·Developer Tools·Role Blurring·Emerging Roles·Skills & Learning·Org Transformation·Labor Market·Big Tech·AI Infrastructure·Manufacturing AI·AI StrategyUse the labels verbatim — the script silently drops anything outside the vocabulary, so a typo means a missing tag.
-
Run (the script writes to the unified Articles DB — its id is a built-in constant, so only
NOTION_API_KEYis needed). Pass the chosen tags as a comma-separated--tags; the script validates them against the vocabulary and fillsCategoriesMulti(the many-to-many field) + the primaryCategory:NOTION_API_KEY="<credentials['notion.integration_token'].apiKey from your task>" \ node workforce/skills/article-level2/publish-notion.mjs \ --author "<agent_slug>" \ --type explanation \ --status ready \ --body-file /tmp/l2-article.md \ --body-en-file /tmp/l2-article.en.md \ --abstract-file /tmp/l2-abstract.txt \ --abstract-en-file /tmp/l2-abstract.en.txt \ --tags "Verification & Trust,Engineering Process" \ --source-url "<sourceUrl from step 1>" # omit if none--body-en-fileis required. There is no Japanese-only publish path — an article without its English edition is an incomplete fire (ADR-0005). -
Report the script's exit code:
0— page created, in both editions. The row carriesAuthor={agent_slug}, Type=explanation, Status=ready, plusAbstract+Category/CategoriesMulti, and anENchild page holding the English edition. Done —Status=readyis already publishable; nothing flips it later.2— W-1 editorial guard failed in either edition (empty/short body, LLM-artefact prelude, or a last line that looks cut off mid-content — the sharedscripts/lib/truncation.mjsheuristic), or401/403auth (project credential bag misconfigured). Read stderr; the message names which edition. Nothing was written, so fix that edition and re-run the whole command. Do not retry blindly. A sentence ending wrapped in emphasis (*…。*) is a valid ending — if the guard trips, the body really is cut off; regenerate the ending.1/3— bad args / missing H1 title in either edition, or Notion API / network error. On3nothing was created.4— the row was created but its English edition failed to write. The article is live and Japanese-only. Do not re-run the publish command — that would create a duplicate row. stderr names the page; report the failure so the operator can complete it withbackfill-en.mjs.
NOTION_API_KEY comes from your task's injected
credentials["notion.integration_token"].apiKey — never read it from anywhere
else, never hard-code it. (The DB ids are non-secret constants inside the
scripts.) The script re-runs the W-1 guards before writing, so a degraded body
fails loudly rather than landing on the site.
The page lands directly in Notion. No PR, no human-approval gate. The page is
written to the unified Articles DB with the live schema (Title, Author and
SourceURLs as rich_text, Type/Status as select, Date) — the same
property contract as the GAS L2 write. newsletter/pipeline/fetch-notion.mjs
exports every row of the unified DB on each deploy — there is no Status
filter and no L4 promotion step (the GAS L4 batch that once flipped ready →
published was retired with the rest of the GAS engine), so the row goes live at
the next deploy-article-site.yml run. That job also surfaces Author + Type
into the front-end manifest so AuthorChip renders the byline.
When NOT to use
- A source already covered by an existing L2 explanation — skip and pick the next uncovered entry (the GAS coverage check keys on source URL).
- A test/placeholder source (
example.com) — never flows to L2. - A source whose original text is a one-line headline only — escalate, do not pad.