penguin-harness-writing
Skill2.9k starsChanged 53 days agoApache-2.0View on GitHub
Use when writing or revising reader-facing prose for penguin.ooo — a blog post of any category, a docs page, or landing-page copy: the division of labour (a gateway model writes every reader-facing word, agents research, prompt, assemble, draw, check and ship), the outline-then-whole-draft flow with budgets and a transition round (section mode as the fallback), the FACTS guard and sparse footnote citations, the profiles, inline SVG figures drawn from the model's figure blocks, the checks, and how it is verified.
- Reads credentials
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 101
All work happens in a scratch directory, one folder per post: <scratch>/articles/<slug>/ for inputs, the outline, the notes and the drafts, …
- Very specific: line 424
The step guards: run when an answer arrives, before it is written.
- Very specific: line 471
Manual-paste fallback: When assemble cannot run, concatenate the latest section files in order (cat, one blank line between them) into …
- Very specific: line 517
When the author asks for the suite on another machine, penguin-harness-dev/reference/verification.md has the remote runner and its traps.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Writing for penguin.ooo
Posts, docs pages and landing copy are written by a model on the TokenDance gateway and shipped
by agents. A post is written the way a person writes one: an outline first, then the whole post in
one call, so its sections join; the review, the fixes, the cuts and the translation work on the
whole post too, and a round of its own edits only the sentences at the headings. A long post or a docs page can fall back to section mode. This page is the contract; scripts/prose.mjs is
the only way text reaches a draft, and prompts/ plus reference/ hold every word sent to the
model. penguin-harness-dev/reference/authoring.md covers what is true of every post (the
bilingual pair, frontmatter, where screenshots live).
1. Who writes what
The API model writes every word a reader sees: the outline and its plan, every section, the
title and excerpt, every string inside a figure, alt text and captions, the second language, every
docs sentence and landing string, and every later fix or cut. Each is its own API call through
prose.mjs.
Agents never type a sentence into the text. They research, compile FACTS, write prompts and
findings, put the model's text together, draw figures from the model's figure blocks, take
screenshots, run the checks, review blind and ship. Putting the text into the manuscript is also
the agent's job, and it is mechanical: write saves the draft, assemble joins section files in
section mode, and strip publishes them. A wrong sentence goes back through a fix call with a numbered finding,
never through an editor. The only hand edits are mechanical: frontmatter date / category,
screenshot paths and their -en / -zh suffixes, whitespace, the boilerplate version note copied
verbatim from an existing post, and copying landing strings into the dictionaries verbatim.
The key never enters the repo. It sits in a chmod-600 file outside the tree; the script reads
TOKENDANCE_API_KEY or the file named by TOKENDANCE_KEY_FILE, never an argument, and prints
neither. Raw responses (with the model's reasoning) are saved in a scratch directory and never
committed.
The default model is seed-2.1-pro, a reasoning model: a call can take minutes, its reasoning
counts against max_tokens (32000 by default), and finish_reason: length is a failure. The key
has a concurrency cap; a post's calls run one after another, and the script's 429 backoff absorbs
the rest. --model overrides the default per call.
2. Profiles
What differs by kind of text lives in one profile, reference/profile-<name>.md, chosen with
--profile <name> (or --category, its shorthand; by default the input's frontmatter decides):
| Profile | For | Contract |
blog-perspectives | a perspective post: an essay on one thesis in connected paragraphs, a lead, a TL;DR, a few sections | prompts/contract-section.md (per section), contract-post.md |
blog-practice | a technical post argued as an essay: a design thesis, sections that each argue one point with evidence and answer the design not taken, numbers rules, TL;DR | the same |
docs | a page of packages/docs/content/, Obsidian-Help style | prompts/contract-docs.md |
landing | strings for the landing page's dictionaries | prompts/contract-landing.md |
A blog category without a profile file needs --profile <file> until one is written;
--profile <file> also tries a profile before it lands. The file names its profile by its name
(profile-blog-practice.md, or profile-practice.md for a blog category) or by the --profile
<name> its header says it is loaded for, and the script says which profile it loaded.
A profile holds the reader, the shape, the sizes and any rule that overrides the style guide for
that kind of text, inside <!-- prompt --> markers (a <!-- prompt zh --> block, when present,
replaces it for Chinese). The model receives it after the style guide, introduced as winning where
the two differ. A profile does not repeat the style guide and states nothing about the product:
facts come from FACTS only. Its ## Check settings are what the script enforces —
sections, lead, tldr, exclamations, and the budgets words, lead-words,
section-words, zh-chars, zh-lead-chars, zh-section-chars, tldr-words, zh-tldr-chars, each
floor-cap, emphasis with its caps emphasis-words / zh-emphasis-chars (section 4),
sources, the floor and cap of a post's reference list (section 7), body-lists and
body-tables (none makes a list outside the TL;DR, or a Markdown table, an error, for a post
written as an essay in paragraphs), and ui-labels: quoted in a post, where bold is only the emphasized sentence and a UI label goes in
quotes ("…" / 「…」), bold in a docs page.
A prompt rule with no matching setting is a rule check cannot see; change the two together.
3. Voice
reference/en-style.md and reference/zh-style.md hold the voice and the banned lists for every
kind of text; the outline, section, translate, polish, fix and review calls all receive them.
One rule there asks the model to write for a reader with dyslexia or ADHD, a person who reads
with effort and loses the thread easily: the point first, short sentences with one idea each, common words, no asides or "by the
way" detours, no defensive hedging, no padding. Stating the reader that way measurably shortens
the model's output and keeps it on its point. The one habit it does not remove is the
"Yes, but…" / 「是的,但是……」 concession pivot, so that pivot is banned by name next to the
"not X but Y" pivot, and check and review look for it. For the same reader a post opens with a
TL;DR of its key points, and one sentence per section is marked in bold by emphasize; the writer
bolds nothing else.
The tip this rule comes from (the author's post, verbatim):
亲测超级好用😂 | 之前为了矫正大模型的说话问题(RL不是没有代价的,你就是代价……),我整过很多版本的工作要求,最极端的是输出前多加一步子Agent专门负责矫正Agent汇报……,然而,真的不如假装残疾人好使。方法就一行⬇️【直接在Agent.md里加入,假设用户是阅读障碍。】⬆️大模型就会说人话了,也不长篇大论了,也会捡重点的说了,也不防御性写作了,也不BTW了唯一扳不过来的是【Yes, But】这个有主意朋友可以跟我说说。现在 LLM 也很难低成本私有化部署和调试,各家又都是冲着把人干掉去训练的,结果现在工作就是个可悲的【人机打架】的L3阶段。而且就DeepSeek而言,我老觉得他有点文本代码化的倾向,就是完全没有脑子……比之前还没有那种,不做真正的思考,纯无脑执行那种。
The rule keeps the tip's one line, assume a reader with dyslexia, and turns the rest of its report into bans: its "BTW" detours and its defensive writing are the aside and hedging bans, and the "Yes, but" pivot the tip could not remove is banned by name.
The zh style guide keeps Harness and Agent Harness in English with that capitalisation, never
「夹具」「线束」「挽具」; a test fixture is 合成测试, 测试数据 or 测试配置; check makes 夹具 an error and a
lowercase "harness" a warning.
4. The pipeline
All work happens in a scratch directory, one folder per post: <scratch>/articles/<slug>/ for
inputs, the outline, the notes and the drafts, <scratch>/articles/raw/ for raw responses
(--raw-dir). The folder name is the slug the figure ids carry; the .en. / .zh. token in a
file name is how the script knows the language. A post is written as one document:
| # | Step | Who | Notes |
| 1 | Research, FACTS, notes | agents | facts.md (section 6), each fact tagged with its sources; notes.md, the author's settled wording |
| 2 | outline | API | thesis, title candidates, section bullets, the figure plan, and the # Plan table: one row per section with its size, facts, opinions, figures and the point it lands on |
| 3 | plan | script | checks the plan against the profile's budgets, the manifest and FACTS; a failure writes outline-feedback.auto.md for one rerun (outline --feedback … --previous …); a second failure goes to the author. plan --freeze writes outline.final.md |
| 4 | write | API | the whole post in one call from the frozen outline: frontmatter, lead, TL;DR, every section and its join to the next, the figure blocks. --title pins the title, --notes passes the notes, --reuse-figures <frozen draft> the blocks already drawn; its report names the next step. A wrong shape is written again with --feedback … --previous <the attempt> |
| 5 | check --plan-rows advisory | script | the page cap and the profile's caps are hard, the plan's rows are targets; over a cap → trim on the whole draft |
| 6 | review + truth review | API + agent | the whole post, flow first: every join after a heading is scored (transition, signpost), and <out>.flow.txt holds transitions: n of m; the agent reads against FACTS, the sources and the notes; merged and numbered |
| 7 | fix | API | the whole post, held to its findings (section 10). At most two rounds, the second only when the first had transition, flow or truth findings, then the author; trim if a cap breaks |
| 8 | transition, then review | API | the joins alone: for each ## heading, the first sentence after it and, when the join needs it, the last sentence before it; the review's transition / signpost findings go along (--findings), and the guard refuses any other change (section 10). The new review's transitions: n of m is the acceptance number: m of m, else the author decides. Once per language, after its last fix and before emphasize |
| 9 | translate | API | the whole post in one call (section 8) |
| 10 | check, review --tells + native reader, fix | script + API + agent | the same loop in the second language, at most two rounds; trim if over |
| 11 | transition, then review | API | the joins of the second language, as in step 8 |
| 12 | emphasize | API | on each language's frozen draft, the first language first, then the second with --mirror <the first's emphasized draft>: one ** span per section and in the lead, nothing else changed |
| 13 | pair | script | section by section: markers, citations, emphasis, figure blocks, code spans, URLs; numbers compared; Chinese size against the English |
| 14 | Draw | agent | from the frozen figure blocks (section 9); a block write or translate changed is redrawn |
| 15 | check --svg | script | every drawn file against its block |
| 16 | strip | script | citations, inline figures, screenshot lines; the first language, then the second with --numbering-from when its citations come in the same order (section 7) |
| 17 | Visual check, verify, PR | agent | sections 9 and 12 |
Flow. The post is one argument written in one sitting. The first sentence after every ##
heading continues the last sentence before it; no section opens with a signpost ("In this
section", "Next,", 「接下来」) or ends on a sentence that only announces the next heading. The plan's
ends with column names the point a section lands on, not a sentence to write. write states
this, review scores every join, transition mends the joins that fail without touching anything
else, and check warns on the signposts the style guides list. A post's joins get a round of
their own because a fix round aimed at the whole text leaves them broken: the author's rule is to
write the whole post, revise it as a whole, then edit only the sentences at the headings.
Emphasis. A profile with emphasis: yes gets one marked key sentence, a whole one, per ##
section and one in the lead: the claim a reader skimming the section must not miss. emphasize
adds the ** pairs to a frozen draft and changes nothing else; --notes passes the author's
notes as guidance on which sentence to mark. The script refuses an answer that
differs from its input once the ** are removed, has no span or two in a section, holds a span
over emphasis-words / zh-emphasis-chars or over part of a sentence (a span opens where its
sentence opens and closes right before its final punctuation), or puts one in a heading, figure block, caption,
version note, TL;DR, code span or link, or around a marker. The closing ** goes before the
sentence's punctuation and markers, so the citation follows it there (**claim**[^1].,
**句子**[^1]。), and the script refuses the other order: after Chinese punctuation a closing **
is not read as bold (**关键句。**后文 shows its asterisks). Bold in a post is only that sentence:
the writing prompts put a UI label in quotes, and check makes any other bold an error. A text
that changes after emphasize is assembled and emphasized again.
The ordering rule. The outline is frozen before the text is written; the text of a language is
frozen before any figure of it is drawn; and no string inside a figure is ours. A figure that
needs a string its block lacks goes back through a fix finding.
Section mode
A post well over a thousand words, or a docs page written task by task, can be written one section
per call instead. section writes plan row n from the frozen outline, seeing the plan, the
sections already written and only that row's facts, into sections/s<n>.<lang>.v<k>.md, and
check --section follows every call (size alone → trim, shape → section --feedback, wording →
fix). assemble joins the latest section files byte for byte, or the versions --pin
s<n>=v<k> names. route splits merged findings by the section that quotes each one, and fix,
translate, polish, review and trim then run per section, with the neighbours read-only.
Their prompts aim at the lowest of the planned size, what the page has left and the profile's cap
(section 5), and --page <assembled draft> tells them which version of each other section the
page holds. From emphasize on, the flow is the same. Section mode trades the joins for control,
so a post written this way needs its transitions checked by review on the assembled draft and
mended by transition.
5. Budgets
Sizes are set three times, so a post cannot grow past them unnoticed:
- The plan. The outline's
# Plantable gives every section a number, in English words or Chinese characters (Chinese-first posts plan in characters).planrejects a table whose rows exceed the profile's section cap, whose lead leaves the lead range, whose sum exceeds the total cap, or whose section count leaves the profile's range. - The prompt. A whole-post
write,translate,fixorpolishstates the profile's caps for the whole, the lead, each section and the TL;DR, andwritetakes the plan's row sizes as targets; each prints, after its call, every section against its cap and plan and the page against its cap. In section mode, every call that writes or changes a section (section,translate,polish,fix,trim) asks for the lowest of three sizes: the section's planned size (from a plan in the other language's unit, converted at the translation ratio), what the page has left once the other sections take theirs, and the profile's cap. The other sections count as--page <assembled draft>holds them, so a pinned older version counts and a newer file set aside does not; without--page, the newest file of each, or the planned size of one not yet written. It never offerscheck's 1.1 × tolerance, so a round of fixes cannot walk every section up to it and the page over its cap.polish,fixandtrimalso state the section's current size, andoutlineand a whole-piecewritethe profile's caps. Each states the counting rulecheckapplies, in words and with an example sentence the script counts itself, so a cap is one number for the model and forcheck. The wording is thecount-rule-<lang>fragment; change it withcountBody. check. A section abovemin(plan × 1.1, profile cap)is an error; with--plan-rows advisory, the mode for a post written as one document, a section above its plan row is a warning and only the profile's cap an error. A lead above its cap and a whole post above the total cap are errors either way. Without--outline, the profile caps apply. Below a floor is a warning: short is allowed, empty is not. On a section file it also reports the page total, counted the same way as the prompt's page budget (with--pagetoo), names where the other sections' sizes came from, and warns when the total is over the page cap; every per-section API step prints the same line, so creep shows before assembly.
A TL;DR, in a profile that has one (tldr: yes), is not part of the lead: the **TL;DR** line
and its bullets are counted apart and held to tldr-words / zh-tldr-chars, the plan's row 0
sizes the lead alone, and the lead's section call states the TL;DR budget separately.
Over budget. A whole draft over a cap goes through trim --in draft.<lang>.vN.md, which
names the sections to cut and asks for just enough. A section over its profile cap aims at 95 % of
that cap, never at its plan row. If the page is still over its cap, the rest of the cut, down to
95 % of the page cap, is shared among the other sections in proportion to their size; the lead is
left alone. Each named section has a floor at 90 % of its target. The prompt asks for rephrasing
inside complete sentences and keeps every fact: numbers with their units, approximations
("about", 约), attributions, and qualifiers as strong as they are ("many" never becomes "most").
trim refuses an answer that changes any other section, leaves a named section or the page over
its cap, cuts a named section below its floor, or loses a number the text stated, so a telegraphic
cut is rerun, not shipped. Figure blocks are not the model's to cut: a block the answer changed is
put back from the draft before the guard runs. In section mode, a section whose
only problem is its size goes through trim --in <section> --outline <outline> (or --cap <n>).
Either way the call may only delete words or put a shorter phrase in place of a longer one. The script refuses the answer when it is still over the cap, or when a
marker, a footnote reference, a figure block, a heading, the frontmatter or the version note
changed: the answer is
kept as <out>.rejected.md, the next version is not written, and the step exits 1. Retry once,
then hand it to the author. With --page <assembled draft>, trim also cuts a section that is
within its own cap to what the page has left when the page is over its cap; without it, the newest
section files only produce a note. A section wrong in shape or content is rerun with section --feedback;
rerunning it only to shorten it brings back errors already fixed, and so does writing a whole post
again only to shorten it. Nobody cuts a section by hand.
Counting is the same everywhere: English words are whitespace tokens holding a letter or digit,
Chinese characters are non-whitespace characters once the Markdown syntax is removed, so every
punctuation mark and every letter of a Latin word counts; frontmatter, headings, figure blocks,
markers and the references do not count. The numbers themselves live in each profile's
## Check settings.
6. The FACTS file and the markers
facts.md has these sections, each a ## heading:
## FACTS— atomic, numbered facts, each ending with its source tags in priority order, the primary source first:- F12: … [S3]or[S3 S4]. Only the first public source of a fact is ever cited (section 7), so order the tags deliberately.## SOURCES— one line per source:- S3: <title> · <url>,· privatefor a source that is never cited (the design repo),· nolinkfor a public source without a URL. A title is data: the page's own title, a repo file's path,PR #N: <PR title>,PenguinHarness docs: <page>, copied as it is, never reworded. Repo file links are pinned to the release the version note names (blob/v<version>/<path>, aftergit ls-treeconfirms the file is there);blob/mainis refused.## OPINIONS— the author's thesis points, ours to assert:- O1: ….## TERMS— spelling, the zh rendering and the UI labels, as a table whose first column is the term; a docs page's TERMS carries the labels exactly as the product's dictionaries spell them.## NOT-FACTS— claims the model must not make.## EXISTING-FIGURES— published screenshots the outline may reuse, by stem.## NOCITE(optional) — facts the writer still marks but that cite nothing:- F12: <why>. Section 7 says what is cited; list here general background, a restatement under another source, and every repo file or docs page the post only refers to. Record the choices in the PR body.
The author's notes. A post may keep a notes.md beside facts.md for wording settled with the
author, one line each: keep[ en|zh]: <exact string> (the answer must carry it verbatim),
avoid[ en|zh]: <string> (the answer must never hold it) and note: <text> (a point the prompt
states). A line tagged with a language applies to that language alone. write, translate,
review and fix take the file with --notes; write, translate and fix refuse an answer
that breaks a keep: or avoid: line, and review flags a sentence that breaks a note. Keep
keep: lines to the few clauses the author settled; everything else is a note:.
The writer makes a factual claim only when FACTS holds it. A section call ends every factual
sentence with its id ({F3}, several as {F3}{F7}) and every position with an OPINIONS id
({O1}); headings, figure blocks and frontmatter carry none. translate and polish keep the
markers as they are and never add one; fix adds one only on a sentence a finding changed; the
script prints any marker a call added or dropped. A fact that needs a citation needs a public
source: a fact whose sources are all private may be used, with a warning, and gets no footnote.
7. Citations
The markers become paper-style citations when the post is published, and most sentences carry
none: one footnote per paragraph at most, each source once per post, and about 13 sources per post
as the ceiling (the profile's sources setting). Writers still mark every factual sentence, since
the markers are the truth guard; the citer thins them. strip --facts facts.md reads each marker
group (the markers closing one sentence): of its facts, in the writer's order, the first whose
first public source is not cited yet is the group's offer, and the first group in a paragraph
with an offer becomes the paragraph's one GFM footnote. The paragraph's other groups cite
nothing, so a source they offered stays free for a later paragraph. A list item and a table row
count as paragraphs. Opinions, private sources and facts under ## NOCITE cite nothing.
Cite what a reader would want to check: a third-party number, a verbatim quote, a named paper,
standard or product claim by someone else, our own measurement, and the first mention of a product
behaviour. List repo files and docs pages the post merely refers to under ## NOCITE, and name
the file in a code span instead.
The TL;DR sums up the body, so its markers stay in the draft as anchors and cite nothing; the
body's first use sets the numbers, and a citation in a published TL;DR is an error. The footnote
goes before the punctuation that ends its clause, outside a closing quotation mark, bracket or
bold (text[^1]., 文字[^1]。, "the slope"[^1]., **claim**[^1].); when the punctuation sits
inside the closing mark (slope."), it follows the whole run and check warns. Footnotes are
numbered by first use. The post ends with the list:
[^1]: [PenguinHarness docs: Agent loop](https://penguin.ooo/docs/agent-loop)
[^2]: A title without a linkA URL in the text is an autolink, <https://…>. A bare URL right before a citation is an error:
strip writes the [^n] straight after it, and the site's renderer reads the reference as part of
the link.
The post carries no references heading and no **Sources** line (the older form, now an error):
the site labels the list from its own dictionary and renders the numbers as superscript [n]
that jump to the list and back. The site numbers a footnote by its first use and builds its anchor
from its label, so the labels must run 1, 2, 3 … in the order they first appear; check makes any
other order an error.
Each language is therefore numbered by its own first use. strip --numbering-from <the first
language's post> keeps the first language's numbers when the second language cites the sources in
the same order, and refuses with a clear message when it does not; strip that language without
--numbering-from then. A source cited in only one of the two posts is an error either way
(strip --numbering-from or pair reports it).
check on a published post proves every [^n] is defined and every definition used, the labels
run 1 … N in first-use order, every link is in ## SOURCES, and no citation sits in a figure or a
caption; a citation after the punctuation is an error. On a published post, and on a draft read
through the same citer with --facts, a paragraph or a sentence with two citations is an error,
and so is a post citing more sources than the profile's sources cap; fewer than its floor is a
warning. pair proves both posts cite the same set of sources, and each section the same
sources, whatever numbers each language gave them.
8. Source language and translation
English first by default. Write Chinese first when the title, a meme or the sources are Chinese:
outline --lang zh plans in characters, and the English follows by translation. translate
takes the whole post in one call, with TERMS, the notes (--notes) and the pinned title of the
target language (--title); it keeps the markers at the ends of the same sentences and translates
figure blocks string for string. With --reuse-figures <frozen target draft> --reuse-source <frozen
source draft>, a figure whose source block did not change keeps its frozen translation, so it needs
no redraw. If the translation still reads translated after one fix round, write that language
afresh from the outline (write --lang <tgt>) instead of iterating on it. In section mode,
translation runs per section, in order, each call seeing the plan and the sections already
translated.
9. Figures
Kinds. Diagrams and charts we draw as inline SVG, real product screenshots in the light theme (captured with the e2e harness and its mock LLM, or reused from the published inventory), or a Markdown table when the comparison is words. No AI-generated illustrations, gradients, glows, shadows, emoji or icons, and no invented data: a conceptual comparison is drawn as states or flows, never as a chart with made-up numbers.
The words come from the model. The section that owns a figure writes its block:
```figure id=<stem>
alt: <one sentence: what is in the picture>
caption: <one sentence: what the reader should take from it>
title: <the heading drawn inside the picture, when it needs one>
text:
- <where>: <a string drawn in the figure>
```<where> is a short lowercase English key (box, column, layer, step1, config2, …) telling the
drawer where the string goes. A block with alt and caption only is a screenshot. A string is
data: a placeholder such as <path> in it is escaped by the drawer, never read as HTML.
The manifest's kind column (or the outline's # Figures table) says which figures are
screenshots, because a block cannot: one that gained text: lines reads as drawn. check with
--figures or --outline makes any line but alt and caption in a screenshot's block an
error.
Drawn figures are inline SVG in the post, one file per figure and language,
<scratch>/articles/<slug>/figures/<id>.<lang>.html, holding exactly one
<figure class="post-figure" data-figure="<id>"> with:
<svg class="fig-svg" viewBox="0 0 560 <h>" role="img" aria-labelledby="<slug>-<id>-<lang>-title">whose<title id="…">is the block'salt;- a
<text>/<tspan>leaf for everytext:string, at least once, and no leaf containing a letter (Latin or CJK) that is not atext:string or the block'stitle(numeric-only leaves — digits,%,×,#, punctuation — are exempt, so axis ticks need no listing); - then
<ol class="fig-steps">with one<li>pertext:string in order (visually hidden, read by screen readers), then<figcaption>with the block'scaption.
Marks carry the site's figure classes from packages/landing/src/styles.css (.post-figure,
.fig-svg, the .fig-* text and structure classes, series .fig-s1–.fig-s3 whose meaning the
post's legend assigns), never inline colours; dark mode comes from their html.dark variants. No
blank line inside a figure (a blank line ends a CommonMark HTML block); <, > and & in strings
are escaped; every id starts with <slug>-<id>-<lang>. A chart generated from data is drawn by a
throwaway script in the scratch directory that reads the numbers and the figure block and refuses
to emit a string not in the block; mirror pairs share one script and one scale. zh and en are drawn
from their own blocks, on the same layout, wider where CJK needs it. Read the dataviz skill before
any chart and use the site tokens, not its placeholder palette.
check --in <draft> --svg <figures dir> proves each drawn file against its block, in both
directions, comparing text after unescaping; strip --figures-dir <figures dir> refuses a file
that does not match its block and inlines the rest as one line; check --in <post> --draft
<draft> re-runs the same assertions on the published post.
Screenshots stay in Prism-Shadow/penguin-harness-community under blog-assets/,
referenced as /blog-assets/<name>-<lang>.png; a new one is merged there before the post's PR.
strip writes them as an image line plus an italic caption, escaping <, > and & outside code
spans.
Visual check, on the test host only (a browser runs nowhere else): build the landing
(--build "@prismshadow/penguin-landing..." with the remote runner), serve it with
pnpm --filter @prismshadow/penguin-landing preview --port <p>, and drive a scratch Playwright
script over the post in both languages, light and dark (the html.dark class, set the way the
site's toggle sets it), at a desktop and a phone width. Assert scrollWidth <= innerWidth, no
console or page errors, every .fig-steps hidden and every figure inside the prose column; save
the screenshots and look at them (labels not clipped, CJK inside its boxes, strokes visible in
dark, the references list and a jump to it). curl there may need --noproxy '*'.
10. Checks
check(local, no network) exits 1 on a hard hit. For every kind: frontmatter, the profile's budgets, section range and TL;DR rule, no numbered headings, no emoji, raw HTML or leaked{{SLOT}}s (an autolink,<https://…>or<mailto:…>, is a link, not HTML), markers against FACTS and their sources (--facts), and the language rules — the patterns at the end ofreference/en-style.md/reference/zh-style.mdplus what the script counts (em dashes per section, exclamation marks against the profile, list and paragraph length, CJK–Latin spacing,——, ASCII quotes and colons in Chinese). For one section (--section, or ans<n>.…file): its shape and the figures its plan row owns. For posts also:##only, figure blocks complete and in manifest order (--figures), drawn figures (--svg,--draft), screenshot paths and files (--assets), footnotes on a published post, no**Sources**line, no in-page anchors, no list outside the TL;DR or no table where the profile saysbody-lists: noneorbody-tables: none, the emphasis rule where the profile has one (section 4; a draft beforeemphasizehas no span, and one with any span needs one in every section), and the citation rules of section 7. In every profile, a**that CommonMark cannot read as emphasis is an error: a closing run after punctuation and before a letter, an opening run after a letter and before punctuation. Sentences over 35 English words or 60 Chinese characters are warnings. Landing strings are checked askey: textlines. Warnings are for a person to judge.- The step guards run when an answer arrives, before it is written.
writerefuses an answer whose version note or pinned title differs from the one given, that breaks akeep:oravoid:line of the notes, or that carries a footnote or, where bold is the emphasis, bold beyond the**TL;DR**line; its report lists the shape, the figures against the manifest and the frozen blocks, and the sizes. On a whole post,fixalso refuses a changed heading no finding quotes, a changed title no finding names, and a section added or dropped; on an outline (outline*.md, or a text with# Plan) it validates the plan after the call instead.translaterefuses another title than--titleand a broken note.fixrefuses an answer that brings in a fact or opinion id neither its input nor its findings name, adds, drops or reorders a figure, gives a screenshot's block atitleortext:lines, or changes a figure block no finding names by its id or by quoting its text.trim, on a section or a whole draft, refuses what section 5 lists.fix,polish,translateandtrimrefuse an answer whose version note is not, byte for byte, the one they were given. Before any guard, an answer loses a lone fence line it echoed from the prompt (the steps send the text inside a four-backtick fence), andtrim,transitionandemphasizeput back, by id, any figure block the answer changed; a dropped block is still refused.emphasizerefuses what section 4 lists.transitioncompares the two texts block by block (paragraphs, headings, the TL;DR, figure and code blocks): it refuses an answer that adds, drops, splits or merges a block, changes any block but the paragraph right after a##heading and the paragraph right before one, changes a sentence of those but the first after the heading and the last before it, changes a rewritten sentence's markers (set and order) or its**, changes the frontmatter, or breaks a note. A TL;DR, list, figure or caption next to a heading is no paragraph, so that side of the join stays as it is. A refused answer is kept as<out>.rejected.mdand the step exits 1; a change the findings should allow goes back with a finding that names it. - The fact diff (agent): every number, URL, code span, bold or 「」 label and proper noun resolves to a FACT or TERM; headings and figures match the outline.
- Review: the API
reviewstep (an editor's findings on naturalness, flow, concision, reading effort and figures, in the formfixconsumes, never a rewrite; on the whole post, flow across sections is its main question, every join after a heading is scored, and the closingtransitions: n of mline goes to<out>.flow.txt; per section in section mode) and the agents' blind reviews — a truth reviewer with FACTS and the sources, a native reader for the second language. The API review reads the text between fence lines, apart from its instruction. It expects the**TL;DR**line and the key sentencesemphasizeadds (checkcounts those), takes an objection stated and answered as part of the argument rather than a "Yes, but…" pivot, and never asks for a list where the profile saysbody-lists: none.
Findings, from any reviewer, are numbered and quote the passage:
N. [passage quoted verbatim] — problem — the fact (F#) or rule it violates — what the fix must achieve (not the wording).
In section mode route relies on that quote to find the section; a passage it finds nowhere or
twice goes to findings.unrouted.<lang>.vN.md for the orchestrator.
11. Assembly and publishing
assemble(section mode) picks the latest version of eachs<n>.<lang>.v<k>.md(or--pin s<n>=v<k>), requires sections 0 … N with frontmatter in section 0, refuses a figure owned by two sections, and prints each section's size against its plan and cap.- Manual-paste fallback. When
assemblecannot run, concatenate the latest section files in order (cat, one blank line between them) intodraft.<lang>.vN.md, change nothing, runcheck --in … --outline …, and say in the PR body that assembly was manual. Typing a word into a section file is never the fallback. - A post:
strip --in <frozen draft> --out packages/landing/content/blog/<slug>.<lang>.md --facts facts.md --figures-dir <figures dir> [--assets <community>/blog-assets], the first language first, then the second with--numbering-fromthe first's post (or without it when its citations come in another order, see section 7). It prints the model's notes (<!-- omitted: … -->,<!-- not applied: … -->,<!-- figure changed: … -->) so they are read. Thencheck --in <post> --draft <draft> --facts facts.md --pair <other post>.packages/landing/test/blog.test.tspins post order per category, so a new post edits that list. Add a changelog pair (Type: process,Scope: landing) perpenguin-harness-dev/reference/changelog.md. - A published post back into a draft:
unstrip --in packages/landing/content/blog/<slug>.<lang>.md --out-dir <scratch>/articles/<slug>/writesrecovered.<lang>.md(each inline figure and each screenshot back as its figure block, each[^n]back as a fact marker after its punctuation),figures/<id>.<lang>.html(each inline figure as published, socheck --svgandstrip --figures-dirwork without a redraw) andfacts.<lang>.seed.md(## SOURCESfrom the reference list, verbatim;## FACTSone line per citation, the sentence that carried it). The second language takes--pair <the first's recovered draft> --facts <its seed>, so both drafts carry the same source and fact ids section by section. Its guard proves every figure against its block and strips the draft with the seed: figure and image lines must come back byte for byte, every other line once citations are removed; it then reports how many citations the current citer keeps. The seed is a starting point: merge it intofacts.mdand verify each fact again. - A docs page:
write(one call) to a draft,striptopackages/docs/content/<slug>.<lang>.md; a new page is also added tosrc/lib/nav.ts, which the docs package's content test keeps in sync. - Landing strings:
writewith--profile landing; eachkey: textline is copied verbatim intostrings-en.tsor the zh dictionary instrings.ts.
12. Verification
Markdown is skipped by format:check; the text is covered by check, by the landing package's
blog tests (content integrity, anchors, links) or the docs package's content tests, and by reading.
A post PR runs the landing tests, format:check and
node .agents/skills/penguin-harness-dev/reference/ai-slop/scan.mjs packages/landing/content/blog.
A post is finished when, on the test host, those pass with git diff --check; check --in <post>
--draft <draft> --facts facts.md --pair <other post> and pair report 0 errors, check --svg
passes, both reference lists are within the profile's sources cap and the last review scores
m of m joins; and the built landing, driven in 8 combinations (en and zh, light and dark, 1280
and 390 px wide), shows every citation as [n] or [n, m] with no two groups touching and none
at a line start, jumps from every [n] to its note below the sticky nav and back with no new tab
or reload, opens the external-link dialog for a reference link, never scrolls sideways, hides
every .fig-steps, holds exactly one bold span in the lead and in each section, carries no 「夹具」
on a zh page and logs no console or page error. A change to this skill runs selftest and
format:check on the test host, git diff --check, and --dry-run of transition and write.
When the author asks for the suite on another machine, penguin-harness-dev/reference/verification.md
has the remote runner and its traps. After changing the script, a contract, a profile's settings or
the check patterns, run node .agents/skills/penguin-harness-writing/scripts/prose.mjs selftest
(no network): it runs the local steps on built-in fixtures, and the API steps' guards against a
stand-in gateway on a loopback port with a dummy key. A case that guards a fixed bug goes there.
13. Files
| Path | What it holds |
scripts/prose.mjs | The runner: API steps outline, section, write, translate, polish, fix, trim, emphasize, transition, review; local steps plan, assemble, route, check, strip, unstrip, pair, selftest. --help lists every option; --dry-run prints the assembled messages without calling or saving |
prompts/<step>.system.md, prompts/<step>.user.md | One prompt pair per API step; {{SLOT}}s filled by the script, and any slot left unfilled aborts the call |
prompts/contract-section.md, contract-post.md, contract-docs.md, contract-landing.md | The output contract for one section (section mode), a whole post, a docs page, landing strings |
<scratch>/articles/<slug>/notes.md | Per post, outside the repo: the author's keep: / avoid: / note: lines (section 6) |
<scratch>/articles/<slug>/recovered.<lang>.md, figures/, facts.<lang>.seed.md | Per post, outside the repo: what unstrip recovers from a published post (section 11) |
prompts/fragments.md | Prompt text used as slot values: the section shapes, marker rules, the counting rules and budgets, the flow rule, the transition step's findings and notes rules, the notes and fixed-title blocks, the trim scopes, the review scopes, the neighbours and translated-context blocks, the version-note rule, the meme brief, feedback blocks, the default title rules, the translation tells |
reference/en-style.md, reference/zh-style.md | Voice and the full banned lists, for every kind of text. The block between the prompt markers is what the model receives; the check patterns below it are what check greps for; change both together |
reference/profile-<name>.md | One per kind of text (section 2) |
Trending
More talk lately.about the whole repository
Who's talking about Prism-Shadow/penguin-harness
- A Hacker News story“PenguinHarness, Open, Efficient, Self-Improving Harness”Hacker News · Jul 23, 2026 · 2 points
- A Hacker News story“PenguinHarness, Harness for Recursive Self-Improvement”Hacker News · Aug 14, 2026 · 1 points
- Prism-Shadow/penguin-harness-benchmark“fix(benchmarks): pin Case task links and launch instructions to commits”GitHub · Oct 29, 2026
- sudosubin/agents.nix“agent-skills.github.prism-shadow.penguin-harness: 0.2.17 → 0.2.18”GitHub · Oct 27, 2026
- NeverENG/SeekMemory“docs: add repository development skill”GitHub · Oct 26, 2026
The biggest 5 of 30 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 30
Talked about on GitHub and Hacker News (30 times).
On GitHub · 28
- Oct 29, 2026fix(benchmarks): pin Case task links and launch instructions to commits — Prism-Shadow/penguin-harness-benchmarksays it tested it0 reactions
- Oct 27, 2026agent-skills.github.prism-shadow.penguin-harness: 0.2.17 → 0.2.18 — sudosubin/agents.nix0 reactions
- Oct 26, 2026docs: add repository development skill — NeverENG/SeekMemory0 reactions · 1 comment
- Oct 25, 2026agent-skills.github.prism-shadow.penguin-harness: 0.2.16 → 0.2.17 — sudosubin/agents.nix0 reactions
- Oct 20, 2026[Candidate] prism-shadow/penguin-harness/.agents/skills/penguin-harness-frontend - penguin-harness-frontend — ProSkillsMD/proskills0 reactions · 1 comment
23 more on GitHub
- Oct 20, 2026[Candidate] prism-shadow/penguin-harness/.agents/skills/penguin-harness-dev - penguin-harness-dev — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 20, 2026docs(skills): draft the rfc-contribution skill — Myriad-Dreamin/penguin-harness0 reactions
- Oct 17, 2026fix(server): a future directory mtime no longer wedges the derived-cache gate — xianull/penguin-harness0 reactions
- Oct 16, 2026Content: PenguinHarness agent evaluation gate — olufemi424/content-viewer0 reactions
- Oct 15, 2026fix(updates): a build whose repository publishes no releases never follows another's — nicolaepocroianu/waf-harness0 reactions
- Oct 14, 2026agent-skills.github.prism-shadow.penguin-harness: 0.2.15 → 0.2.16 — sudosubin/agents.nix0 reactions
- Oct 14, 2026feat: register GPT-6 Luna and preserve reasoning off — Prism-Shadow/model-message-stream-protocol0 reactions
- Oct 14, 2026Discovery watchlist — rishibhaskar777/whichai0 reactions
- Oct 14, 2026agent-skills.github.prism-shadow.penguin-harness: 0.2.13 → 0.2.15 — sudosubin/agents.nix0 reactions
- Oct 1, 2026agent-skills.github.prism-shadow.penguin-harness: init at 0.2.13 — sudosubin/agents.nix0 reactions
- Sep 29, 2026feat(company-proposals): the author can withdraw a proposal that is still drafting — Myriad-Dreamin/penguin-harness0 reactions · 1 comment
- Sep 29, 2026The small-window notice wraps its buttons on a phone instead of crushing the conversation — Myriad-Dreamin/penguin-harness0 reactions
- Sep 29, 2026fix(web): the workspace picker's Use this dir takes the path typed into the box — Myriad-Dreamin/penguin-harness0 reactions
- Sep 29, 2026fix(web): the mode switch, the sidebar and the page stand in one mode — Myriad-Dreamin/penguin-harness0 reactions
- Sep 29, 2026refactor(web): make the terminal the first Web App module — Myriad-Dreamin/penguin-harness0 reactions
- Sep 29, 2026A machine that does not answer is asked once, not about every Agent on every reload, and not again over HTTP — Myriad-Dreamin/penguin-harness0 reactions
- Sep 29, 2026feat(web): the plugins page's search box sits in the header, and its buttons show their words — Myriad-Dreamin/penguin-harness0 reactions
- Sep 29, 2026fix(web): copy works on a plain-HTTP origin, and the check means it happened — Myriad-Dreamin/penguin-harness0 reactions · 1 comment
- Sep 29, 2026Add PenguinHarness — AppImage/appimage.github.io0 reactions · 1 comment
- Sep 26, 2026feat(adapter): add read-only PenguinHarness support — RealZST/HarnessKit0 reactions · 2 comments
- Aug 24, 2026Curate persistent memory and skill evolution resources — YuxingLu613/awesome-agentic-evolution0 reactions
- Jul 23, 2026[docs] promote PenguinHarness in readme — hiyouga/EasyR10 reactions
- Jul 23, 2026[docs] update trend badge and promote PenguinHarness in readme — hiyouga/LlamaFactory0 reactions · 1 comment
On Hacker News · 2
- Jul 23, 2026PenguinHarness, Open, Efficient, Self-Improving Harness2 points
- Aug 14, 2026PenguinHarness, Harness for Recursive Self-Improvement1 point
In this repository3 other files it gives its agents.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

