Imported from janpolacek/mars-ai-simulation (
.agents/skills/editorial-review/SKILL.md). Install upstream withnpx skills add janpolacek/mars-ai-simulation --skill editorial-review. Copyright stays with the author.
Editorial review
Required inputs: the claimed kanban card, article draft, source pack, asset
manifest and candidate assets, brief, and applicable public canon. Read
AGENTS.md, docs/INSTRUCTIONS.md, your own role definition (your agent's SOUL.md —
the mars-ai-simulator-editor profile owns this gate, and since 2026-09-17 it holds the
continuity and release authority too), and
.agents/skills/copy-editing/SKILL.md first.
Allowed tools: repository inspection/editing limited to the review record,
link/check commands, and the hermes kanban worker commands (show, claim,
comment, complete). A worker may only claim, comment on, and complete its own
card; it does not create or reassign cards. Do not deploy, do not touch external
services, and do not silently repair another worker's artifact.
Write .agents/work/reviews/<slug>.md with pass/fail findings for citations, links,
coherence, voice/grammar, image placement/captions/alt text, and metadata/schema.
Set its final status to approved, changes_requested, or blocked.
Reject and keep the card open for any material factual, accessibility, licensing,
or coherence failure. Create or request a scoped corrective card and link it as a
blocking dependency of this review card
(hermes kanban link <CORRECTIVE> <REVIEW>); this skill may create that one card.
An approved review is the editorial gate and the release decision: the merged
editorial role confirms the package against released canon and records that it may be
published, with no separate human approval sentence. Run the continuity check in the
same pass (chronology, canon consistency, plausibility, spoiler safety — record it in
.agents/work/continuity/<slug>.md) unless an upstream card already carries a current
continuity clear for the same package hash. Record the result, evidence paths, the exact
public scope, and the release decision as a card comment (hermes kanban comment <id> "...")
and in .agents/work/reviews/<slug>.md before hermes kanban complete.
The simulated record date
Every published article carries exactly one simulated record date — the date, inside the
fiction, on which the article was written. This role owns it: no writer, SEO, visuals or technical
role decides it, and the article's real repository history is not part of it. It is a frontmatter
field (simulatedDate in website/news/<slug>.mdx), so it lands on the same surface as the
publication flip and needs no separate card.
- Read the milestone table in
docs/SCENARIO.md("Launch and mission dates") and pick the milestone the article's released timeline step covers — step 001 the programme announcement and charter, step 002 the payload and concept-review work, step 003 the design freeze, and the matching milestone for each later step. - Use a date that table actually holds. "Roughly matching the timeline" means the item sits on a
locked milestone in the timeline's order — never an invented day, month, or year that merely
looks plausible. If no locked milestone fits the step, leave the field unset and name the gap on
the card; do not fill it from imagination, because inventing a calendar date is new scenario
canon and belongs to the human story owner (
docs/timeline/README.mdsays the same). - Check the value before you apply it: it implies no event the step has not released; it cannot be
read as a launch, landing, or other withheld mission date; it contradicts no sentence already
public (an article that says "no launch or landing date has been stated" still holds, because a
record date is not a mission date); and the articles stay in timeline order by the dates they
carry, not only by their
orderfield. - Verify the published surface, not only the file: the built
/news/<slug>/page states the date, every label or string around it presents it as part of the fiction, and it is nowhere presented as a real publication date, adatePublished, or alastmod. - Record the value and the
docs/SCENARIO.mdline it came from in.agents/work/reviews/<slug>.md, and name both in the release decision and the card comment.
Verified practice
-
Verify the published surface, not only the article file. An MDX body can pass every gate while the page that carries it leaks: check the site chrome under
website/src/pages/and inspect the build output (website/dist/) for gated names, imagery, and unexplained values. Report surface failures in the same record — the gate covers the published package, not just the copy. -
Verify asset provenance by content, not by path: compare
sha256sumof the file actually in use against the approved hash indocs/brand/BRAND.md. A re-exported or transparent-background variant of the same artwork still fails the approved-hash check and needs a human release decision. -
Re-read the gate inputs and re-run
git statusimmediately before writing the verdict. The working directory is shared with other profiles, so the scenario file, timeline steps, and asset paths can change mid-run; never assert a broken reference or a stale path from a snapshot taken earlier in the run. -
Corrective card mechanics: create it as
<CORRECTIVE>, link it as the parent of the review (hermes kanban link <CORRECTIVE> <REVIEW>), then block the review card with kinddependencyso it resumes when the corrective completes. Write the corrective body with withheld-by-default actions and an explicit "not yours to decide" list, so no agent settles a canon question on the way through. -
Cards and record templates name paths that a concurrent refactor may have deleted. Resolve a missing input to its current equivalent and state the substitution in the verdict (article 001:
website/scripts/sync-assets.mjsis gone; the surface work now lives inscripts/guards.mjs+scripts/check-dist.mjssrc/lib/assets.ts). Never report a superseded path as a broken reference or gate on it.
-
Prove whether the article body is public at all before reviewing its surface: the frontmatter
publicationpredicate (src/lib/publication.ts+src/features/news/query.ts) plus the route list printed bywebsite/scripts/check-dist.mjsshow that only the card, not the body, is reachable while the slug isdraft. (The oldersrc/lib/releases.tsslug list was retired in commit2d9ee0d.) -
Name withheld material by location, not by value. Cite the gated-term list (
.agents/work/briefs/<slug>.md"Forbidden token set", the review record) and report hit counts; keep scratch scanners outside the repository so the tree gains no new copy of a withheld name, coordinate, date or provider. -
In this shared checkout a concurrent worker proving the build guard can transiently plant a withheld asset in
website/public/assets/— gitignored, so invisible togit status.check-distfails closed while it is there and passes once it is removed. Re-run the surface check immediately before writing the verdict, record the observation with timestamps and hashes, and do not raise a corrective card for a test artefact that clears itself. -
A gate card can be dispatched before its parents finish, and the planner may link a parent mid-run.
kanban_completethen returns a generic "could not complete … (unknown id, stale run, or already terminal)" and writes nothing — the cause is the parent gate, not the verdict. Check the card's parents before assuming a tool fault: wait for the parent (a bounded read-only poll oftasksin~/.hermes/kanban.dbis enough) and re-verify the package, orkanban_block(kind="dependency"). Do not reach forhermes kanban complete --forceto close your own card — it bypasses the live-claim guard — and note that the CLI refuses outright in a session carryingHERMES_DELEGATED_CHILD_CONTEXT. -
A slug's gate record is one file re-written by each pass, and downstream records cite its hash. When a re-check supersedes an earlier version, keep a revision-history section naming the superseded hash and what moved around it, so a later reader does not read the mismatch as tampering. Re-hash the article first: an unchanged article hash is what lets a superseding verdict stand on an earlier gate's reading of the same bytes.
-
A surface string another card owns can change during your gate — and change back. On the
/about/gate the/news/document title moved twice inside one run (News | Red Horizonfrom a dev card, then the newsroom string restored on an SEO re-pass) while two cards argued it. Judge the revision live at verdict time, quote it, and record every revision with its hash and timestamp; then state explicitly whether your approval depends on that string at all (there it did not). A verdict row quoting a stale surface string is the failure mode; a record that names the revision and the movement is not. Re-read the built output (website/dist/) after any such movement — the page source and the built file are not the same surface. -
When the package's canon cannot be confirmed — a material claim traces to no released material, or publishing it would require a later timeline step to become public — the gate must not complete: the card body forbids closing on an unconfirmed claim, and a completion would promote the BUILD child into a package no gate has passed. Record the findings, set
changes_requested, and create the corrective card inblockedstate (kanban_create(..., initial_status="blocked")) whenever it cannot start before the release step moves, link it as the gate's parent (never as a child —parents=[your-gate]deadlocks both directions), and callkanban_block(kind="dependency"). Advancing the release step itself is the human story owner's call, not the merged role's: write the question into the corrective body with its "not yours to decide" list, and keep the gate-side required changes as C-items. Confirming that a package agrees with what is already released is the merged role's own decision and needs no such card. -
The release / release-decision card — the card that carries the
publication:flip — is the merged editorial role's own decision, not a human gate. Record the decision on the card itself: the exact public scope, the evidence, the article hash at the flip, and any condition. A board scan for approval sentences the planner's template promised is no longer the check — check instead that the release decision exists and is the editorial role's, that the article hash matches the approved revision, and that the builtwebsite/dist/carries the route and no withheld material. Withhold the flip andkanban_block(kind="needs_input")only when the claim needs canon the human has not released; say plainly which sentence or step is missing. Completing instead would promote the downstream BUILD child into a package no gate passed. -
A card written as a human approval gate can be reassigned to this role mid-run by an operator authority change, on top of a human answer that is already on the card. Then: record the human answer as a human record (quoted, with its timestamp), and record the decision in this role's own sentence — the card body's template sentence stays unapplied whenever there is nothing to release (no package, no date), because applying it manufactures an approval nobody made. A gate with no package still needs a verdict, and the honest one is withhold with the fail-closed default verified in fact: name the artifact that does not exist and measure the live surface (routes 404, empty index, no sitemap) instead of restating the card body. Where a card body's condition has been superseded (it still demands a human sentence), route the wording fix to the owning role rather than editing their card, and create no downstream card for work that cannot start — write the precondition (e.g. "the release card must exist before any flip") into the card that owns the pipeline.
-
Verify the fail-closed state in fact, not only on paper: the article hash must still equal the approved revision, and the built
website/dist/must contain no route and no reference for the slug. A draft that is merely declareddraftis not proof that it is not public. -
A card body's placement instruction can name the wrong media key. Check it against the contract in
src/lib/media.ts(plateCount/altCount/captionCount) and the registry insrc/features/news/media.tsbefore applying it: a key whose plate count differs from the asset the card means belongs to another article. Correct the instruction in the record, cite the registry and the canon line that releases the other key, and never place another article's plates just to make the field valid. -
The release mechanism itself can be retired mid-run:
src/lib/releases.tswas deleted in commit2d9ee0d, leaving the frontmatterpublicationfield as the single predicate for listing, card and route. The frontmatter flip stays the correct release action, and older handoffs telling a worker to "record an entry insrc/lib/releases.ts" are superseded, not broken. -
A re-run of that card after unblocking turns on sentence fidelity per gate, not on the gate count. A human may answer in their own words ("1 gather media … automatically approve if editor is ok with it; 3 ; 4 - handle it") instead of the template sentences the card body required, with an operator comment reading the answer gate by gate; record which gates arrived verifiably verbatim and which did not, and work from the operator's recorded reading rather than inventing a sentence the human never said. Establish it from a board-wide
task_commentsscan that rejects the planner's own templates, and state the fidelity in the ledger itself — a later reader grepping for the template string will find it in the card body and must not read that as the decision. -
An approved asset can be live before its article is. A media key's plate is statically imported in
src/lib/assets.ts, sodist/_astro/carries it even when no route renders it, and wheremainis wired to an automatic build (Cloudflare Workers Build on push) the commit that adds the key serves the artwork while the article still 404s. Measure the live surface from outside at flip time and record it: a directcurlcan be refused by the session's command scanner, so fetch in-page on the live origin and hash withcrypto.subtle(same bytes, same digest) — and measurearrayBuffer().byteLengthover those bytes, not the decoded text: a text length undercounts a page that carries non-ASCII characters, which reads as a content change that never happened. Judge it editorially — ungated filename, no gated marker in alt or label, unlinked from every page, content-hashed URL ⇒ an observation, not a blocker — and route the "should a key's asset be emitted before a route names it" architecture question to the card that already owns it instead of opening a new one. One thing it does change: a rights or licensing item stops being a precondition and becomes live work. -
Never run the build in a release card, for the same pruning reason as a gate card. Validate the flip read-only instead: parse the frontmatter with the parser the pipeline resolves (
js-yamlfromwebsite/node_modules) and callnewsMediaIssues()straight out ofwebsite/src/lib/media.tsfrom a scratch script outside the repository — that checks the real schema rule (key existence, per-keyaltCount/captionCount/requiresLabel) without touchingdist/, and it catches an alt string that differs from the approved one by a single byte. -
Format the record and prove the article was not reformatted.
~/.dprint/bin/dprint fmt <record>from the repository root applies the project style (it aligns tables); the configured plugins match no.mdx, so the article bytes stay exactly the flip — worth stating in the record, since "the file changed" otherwise carries unreviewed formatting. -
Probe the board for concurrently running cards before and after applying a flip. The dev role commits and pushes its own card's scoped change, so a card that staged
website/news/**or pushed the whole tree would publish the article before the build card's route/listing/guard proof. That is released once the editorial decision is recorded, but it is still out of order: record it, name the owner, and flag the article path as a hotspot. -
Gate a site-string deck (a card that transcribes copy into a source file) by hashing the approved form, not only the deck. Record the code-block hash, its file and line range, and one newline-joined hash of every frozen string literal in deck order; that is what makes the transcribing card's "byte-for-byte" acceptance criterion checkable by a third party. Confirm the literals are ASCII first — one curly quote makes a byte comparison fail for a reason that has nothing to do with the copy.
-
Probe the build surface read-only.
website/scripts/check-dist.mjsprunes offending files fromdist/when it fails, so a gate must never runnpm run buildor the postbuild guard to check a package: importcheckDist(),listRoutes()andscanSourceForGatedReferences()fromwebsite/scripts/guards.mjsin a script outside the repository. A review must not be able to delete the shared build it is judging. -
Resolve a deck's own count words against the writer's source card before calling a stale count: a writer's card can require four decisions while the writer records a fifth of its own, which leaves heading and section both defensible and only the ambiguity worth recording. The same mismatch often sits in the gate card's own acceptance criteria — judge every enumerated item either way.
-
Judge link wording from the built page's full anchor inventory, not from the new link alone: extract every
href+ text pair fromdist/**/*.htmland read it as a link list. Two near-identical labels pointing at different destinations are invisible when you read one link at a time. -
When the transcribing card's literal instruction ("no change to the … markup") conflicts with a styling regression another pass measured and already routed, approve with an explicit rendering condition in the record plus a pointer to the routing comment, rather than blocking or raising a corrective card for a string the deck does not own. Say that the condition is implementation-only and does not hold up the chain.
-
Gate a media sign-off (a card that approves one gathered image for a placed band) on measured pixels, not on the manifest's prose. Read each candidate with a vision pass and measure it — void-black fraction, per-quadrant mean luminance, and the label chip's own corner — with a stdlib PNG decoder (zlib + unfilter) kept outside the repository; then composite the chip (
rgba(1,0,0,0.78)over the measured region) and report WCAG contrast. On the 002 media gate the measurement inverted the manifest's own claim about which candidate's label corner was weakest, and the label text held 17–20:1 on all three: cosmetic findings are not accessibility failures, and an impression-based read would have written the claim down wrong. -
Re-verify each proposed alt text against a zoomed vision read before approving it. A prompt constraint can silently fail to render — the 002 slot-03 alt promised "a thin level line of pale light skimming across", and the zoom found a diagonal fissure instead — which leaves an alt text that misdescribes the published image. That mismatch, not the composition, is what withholds approval from an otherwise rule-clean candidate.
-
Approve exactly one named candidate for a band, state the others' verdicts explicitly (
approved/rejected/usable-as-an-alternative only), and hand the dev card a copy command plus the hash the copied file must equal. Check the key's contract insrc/lib/media.tsand what the renderer actually prints: for a single-plate keyset.provenanceis never emitted (onlyplateCaptionText()for plate sets) and the card chip is drawn frommediaLabel, so the "illustrative, not mission photography" statement has to live in the alt text and an unset label renders an empty chip. -
When a candidate rests on a delegated human gate, quote the human's words and the operator's gate-by-gate reading of them, then say plainly whether the choice is yours — an upstream manifest's cautious "the choice is the human's" written before any sign-off existed is superseded by a later recorded relay, not a reason to block.
-
Create a corrective card with an explicit workspace:
kanban_create(..., workspace_kind="dir", workspace_path="/home/janpolacek/Projects/mars-ai-simulator"). Thekanban_createdefault is a scratch workspace, which cannot edit the shared tree, and nothing later repairs it —kanban_blockandkanban_completeare scoped to the caller's own task and this session'shermes kanbanCLI refuses mutations from a delegated child context, so a miscreated card cannot even be archived. Re-create it correctly, comment on the mistake naming the replacement id, and put the one-command human cleanup (hermes kanban archive <duplicate>) in the handoff. -
Check a package's "not settled" claims against the published articles' own wording, not only against the step file. A negative claim that a released article already qualifies ("…has been chosen publicly") turns into a canon-fidelity finding when a newer article drops the qualifier: the unqualified form asserts internal state, and locked canon usually contradicts it (003: the landing target and its geometry are canon, only their publication is withheld). Report it as a claim-level FAIL with the published counter-example as evidence — the fix is the qualifier, and no canon change is needed.
-
Treat a vision read of small hardware detail in a render (joint count, fastener count) as unreliable rather than as evidence: three passes over one 1448×1086 plate returned three different counts. A detail the plate cannot settle does not gate a package; it gates only when a public string (alt text, caption, label) asserts it. Record it as unverified and say what would make it checkable.
-
Re-verify a corrective by rebuilding the earlier bytes yourself, not by trusting the corrective card's revert proof: apply the claimed substitutions in reverse to the corrected file and compare with the hash the earlier pass recorded. A byte-identical reconstruction plus a
diffof exactly the claimed hunks is what lets every claim row that passed before carry over, so the second pass only re-judges the changed items — 003 pass 2: two hunks, 28 rows carried, verdictcontinuity clear. Put your own reconstruction in the record, not the writer's helper. -
Measure the fail-closed state on the live origin and say which surface each claim came from. A local checkout can be many commits behind
origin/main, andorigin/mainis what the push-triggered build deploys: on 003 the localdist/had no/wiki/route while the live origin already served a wiki index, solistRoutes()ondist/was not the live surface. Check both and mark the authoritative table in the record. -
A negative claim ("no landing site has been announced") can be true at verdict time and still be order-sensitive: canon may permit a same-step sibling (the
/wiki/area/landing-zone node) to go public, and that publication falsifies the sentence. Record it as a release-order condition on the clearance, routed to the card that owns both flips and to the release card — never return the article again for a claim that is true against released canon as it stands, because the ordering is a release decision, not a wording one. -
Re-read the guard's marker list against the step being released:
gatedTextMarkerscan still name material the step legitimately releases (RH-01,Pathfinderat 003), so the retirement must ship in the same change as the flip or the production build fails closed. Route it as a release precondition with a named decision card and implementation card, not as a continuity failure. -
A blanket directory retirement also retires whatever per-file protection that directory was giving. Before deciding the retirement, run the real guard module over the exact release inputs and then over the narrowed configuration, and look for what stopped being caught: on 003, dropping
vehiclefromgatedDirectoryNameslet the withheldcontact-arm-scene.pngbe imported with no offence, because the directory rule was the only thing protecting it. The decision must therefore name the still-withheld file and require a per-file rule in both scanners — the withheld-source set thatcheckDist()matches by name/stem/hash, and a new rule inscanSourceForGatedReferences(), which only knows directories and cannot catch the reference on its own. Prove both halves in the suite (retired material passes, the withheld file still fails) and keep the proof table in the record. -
Read the pipeline graph before deciding where the flip is applied. A template can place the deploy card before the release card while
AGENTS.mdrequires the release decision to exist before a push carries content public: on 003 the flip had to ship inside the build card's change, because applyingpublication: publishedalone would leave the shared checkout with a published article whose own postbuild guard fails closed (and any concurrent push would deploy that failure). State on the release card that the decision is recorded and that the build card applies it, so no second, divergent flip appears later. -
The record date field may not exist yet in the schema, and an article published before it did carries no value. That is a gap to fill, not a pass: check
src/content.config.tsfor the field before asserting one, and where it is missing, route the schema-and-render work to the technical role instead of writing a field the build will reject. A frontmatter key the schema does not declare is a build failure, not a harmless extra. -
A gate-ledger card whose gates are canon authorship, release-order or public-identity questions is a hold, not a package verdict.
kanban_completeon it releases its parent-gated children — on the step-004 launch-provider ledger that is the draft card, and a writer handed a blank canon choice invents the provider the gate exists to withhold. So: scan the board for a recorded answer before treating a gate as unanswered (a board-widetask_commentssearch, not just the card's own thread); record every fail-closed default measured rather than restated (the dossier that does not exist, the route that 404s on the live origin, the withheld tokens absent from every served byte, the child card stilltodo); state in the record that the card-body templates are unapplied, so a later reader grepping for them does not read one as a decision; thenkanban_block(kind="needs_input")naming the exact sentences the human story owner owes. The 2026-09-17 authority change moved releasability and canon confirmation to this role; canon authorship, the release-order advance, and the public title/byline stayed with the human, so those gates are not this role's to answer however long the card sits. -
Check whether the withheld strings for the next step have any guard marker at all: on 004,
gatedTextMarkerscarried only the landing-region coordinates, so a provider name or a withheld launch date in public copy would have failed no build, and containment rested on the gates alone. Report the coverage as a release-engineering item with a named owner (a dev card adds a marker, and the step that releases the material retires it in the same change) instead of treating a missing marker as a blocker or as proof the material is safe. -
A present-tense negative claim in an already-published article ("no launch vehicle has been named") is order-sensitive against a later step: true today, stale the moment the step that names it is released, and unfalsifiable only once the article's own record date is on its face. Record it as a release-order condition on the upcoming flip — record dates or a scoped wording corrective, owner named — never as a continuity failure of the unpublished package and never as a reason to return it. Assigning the record dates is the fix when only a flip can stale the claim (the 001–003 record-date card closed the step-004 ledger's C6 that way, with no wording change): state in the record that the claim now reads as a statement about its date, check that an earlier article's negative still holds at the later date, and record the consequence — the step that names the withheld thing must carry a record date after the dated negative, or that negative is false in-fiction. The value you set is therefore a lower bound on the next step's value: say so.
-
Choose the milestone row by matching the state the article itself claims, not by "closest in time". A report of a frozen design cannot be dated on a preliminary-design-review row (the record date would sit before the freeze the article reports), so the step's primary anchor is the row whose meaning is the step's own event (a critical design review for a design freeze). Where a candidate row's text carries an event whose publication is still gated (a target confirmation), that row's text is the reason to exclude it — and judge the public surface as it is: it shows a calendar date under the in-fiction label, never the row's name, so a row whose wording would be a containment problem stays a record-internal association.
-
Verify a frontmatter-only change in a scratch copy outside the repository and run the copy's own gate: copy the site's
src,public,scripts,news,test, the config andpackage.json(plus adocs/symlink, with the installed packages linked one by one so Astro's content-layer cache stays in the throwaway root), then build with the project's own CLI. Two traps in that copy: it has nonode_modules/.bin(dot entries are skipped by design), so run the suite asnpm testornpm test -- <file>rather thannode_modules/.bin/vitest; andtest/must be copied, or vitest exits 1 with "No test files found". Pair a red-first control on the render (a planted value moves that page's line while its siblings hold) with one on the schema (a planted prose value fails the build naming the field), so a green build proves the value was read, not ignored. Scan the wholedist/tree, not one page — the built-site half of the fixture suite is the strongest single measurement — and expect a media-registry provenance sentence to carry an artwork generation date in prose: pre-existing, not a machine-readable date, not the article's record date, and not a failure. -
A step can end up with two gate ledgers for the same five gates when a container card re-creates a graph a planner card has already built. Do not open a parallel record for the second card: append a
## Revision Nsection to the slug's existing record (the superseded hash, the second card id, the fresh measurements) and say plainly which graph the step runs on. Establish that from the card bodies and the edges — the chain whose stage bodies name the other gate id, and which holds the dossier / wiki cards, is authoritative — then hold the duplicate withkanban_block(kind="needs_input")and recommend the operator archive it (board dedupes are theirs;hermes kanban archiveis outside this role's toolset). Completing a duplicate ledger still promotes its own parent-gated draft card into the same blank-canon article, so duplication is a release-safety issue, not board tidiness. Also flag any output path two cards write concurrently (here.agents/work/sources/<slug>.md) as a hotspot with the hash you measured.
Acceptance checks: every review-table row passes, the record's final status is
approved, the article carries one simulatedDate drawn from a milestone line in
docs/SCENARIO.md with that line named in the record, and no material failure remains unresolved.