source-command-review
Skill0 starsChanged 6 months agoMITView on GitHub
Review code changes for bugs, security issues, and improvements — includes cumulative regression suite
- Reads credentials
- Deletes or force-pushes
- Commits and pushes
1 other project uses a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- pollinations/pollinationsedited, first seen Oct 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 1
- pollinations/pollinations★ 5.3k11% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository55 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- agent-developmentSkill
- agent-teamsSkill
- ai-sdkSkill
- analyzing-financial-statementsSkill
- cache-componentsSkill
- chrome-devtoolsSkill
- Codex-automation-recommenderSkill
- Codex-md-improverSkill
- code-reviewSkill
- Command DevelopmentSkill
- creating-financial-modelsSkill
- data-visualizationSkill
- debugSkill
- develop-ai-functions-exampleSkill
- evolve-ai-layerSkill
- excalidraw-diagramSkill
- find-skillsSkill
- frontend-designSkill
- Hook DevelopmentSkill
- make-skill-templateSkill
- markdown-to-htmlSkill
- mcp-builderSkill
- MCP IntegrationSkill
- pdfSkill
- playwrightSkill
- Plugin SettingsSkill
- Plugin StructureSkill
- refactorSkill
- researchSkill
- security-best-practicesSkill
- skill-creatorSkill
- Skill DevelopmentSkill
- smithery-ai-cliSkill
- source-command-commit-and-deploySkill
- source-command-commitSkill
- source-command-create-prdSkill
- source-command-create-rulesSkill
- source-command-empty-redeploySkill
- source-command-evaluateSkill
- source-command-executeSkill
- source-command-handoffSkill
- source-command-plan-featureSkill
- source-command-primeSkill
- source-command-self-healSkill
- source-command-validateSkill
- sql-queriesSkill
- statistical-analysisSkill
- vscode-ext-localizationSkill
- webapp-testingSkill
- web-artifacts-builderSkill
- web-design-guidelinesSkill
- Writing Hookify RulesSkill
- xlsxSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
21 rules. About 0 are common, 14 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈14
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 8
Use this skill when the user asks to run the migrated source command review.
- Distinctive: line 19
Before starting the standard review, check if the most recent contract-bearing plan has a contract:
- Very specific: line 32
If a plan file exists, read its ## Contract section.
- Very specific: line 35
After Phase 3 (Code Review), add a Phase 3.5: Contract Evaluation that spawns a fresh sub-agent to grade the implementation against the …
- Distinctive: line 38
If no contract exists, skip this and proceed normally.
- Very specific: line 54
If src/core/db/schema.ts was modified (new tables, columns, or constraints) but SCHEMA_VERSION was NOT incremented, flag as Critical — …
- Very specific: line 57
Add an if (fromVersion < N) block to the migrate() function in src/core/db/database.ts
- Very specific: line 58
Put the ALTER TABLE or CREATE TABLE statements in the new block
- Very specific: line 59
Update src/core/db/types.ts to match the new columns
- Distinctive: line 70
If this fails, flag as Critical.
- Distinctive: line 74
Run these tests FIRST to verify that all previously shipped features are intact.
- Distinctive: line 466
All tests should pass. If any fail after your changes:
- Distinctive: line 468
Check if you modified the source files listed above
- Distinctive: line 469
Read the failing test to understand what behavior it expects
- Distinctive: line 474
After running the regression suite and before finishing the review, check if any test files touched in this session are NOT already in the …
- Distinctive: line 494
After regression tests pass, run the full suite:
- Distinctive: line 524
If reviewing a specific commit, check it out or diff against it.
- Distinctive: line 541
No emojis in content or user-facing output
- Distinctive: line 546
Use sub-agents to explore the codebase in parallel for efficiency
- Distinctive: line 548
Do NOT report speculative or low-confidence issues — conclusions must be based on actual code understanding
- Distinctive: line 549
If reviewing a specific git commit, note that local code may differ from that commit
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 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
source-command-review
Use this skill when the user asks to run the migrated source command review.
Command Template
Code Review
Perform a thorough code review of the current changes AND run the cumulative regression suite to ensure all previously built features still work.
Phase 0: Contract Check
Before starting the standard review, check if the most recent contract-bearing plan has a contract:
# Find the most recently modified plan file that actually contains a contract.
# Evidence/proof notes may also live in .codex/plans/ and must not mask the feature plan.
for f in $(ls -t .codex/plans/*.md 2>/dev/null); do
if rg -q '^## Contract$' "$f"; then
printf '%s\n' "$f"
break
fi
doneIf a plan file exists, read its ## Contract section. If a contract is found:
- Note the tier and criteria in the review output
- After Phase 3 (Code Review), add a Phase 3.5: Contract Evaluation that spawns a fresh sub-agent to grade the implementation against the contract (see
/evaluatefor the full evaluator protocol) - Include the contract evaluation results in the final output
If no contract exists, skip this and proceed normally. The contract evaluation is additive — it does not replace the standard code review phases.
Phase 0.5: Database Migration Check
This project uses SQLite via better-sqlite3 with schema migrations gated on the user_version pragma (see src/core/db/database.ts). There is no separate migration tool — migrations apply at runtime when getDatabase() is called.
Step 1: Check for schema drift
# Check whether the schema file was modified without bumping SCHEMA_VERSION
git diff HEAD --name-only -- 'src/core/db/schema.ts' 'src/core/db/types.ts' 2>/dev/null
git diff HEAD -- 'src/core/db/schema.ts' | grep -E '^\+.*SCHEMA_VERSION' 2>/dev/nullIf src/core/db/schema.ts was modified (new tables, columns, or constraints) but SCHEMA_VERSION was NOT incremented, flag as Critical — existing databases will not pick up the change. The fix is:
- Bump
SCHEMA_VERSIONby 1 insrc/core/db/schema.ts - Add an
if (fromVersion < N)block to themigrate()function insrc/core/db/database.ts - Put the
ALTER TABLEorCREATE TABLEstatements in the new block - Update
src/core/db/types.tsto match the new columns
Step 2: Apply migrations
Migrations apply automatically on database open. To verify a fresh init works with the current schema:
rm -rf .blog-agent/
npx tsc -p . && node dist/cli/index.js initIf this fails, flag as Critical.
Phase 1: Regression Suite
Run these tests FIRST to verify that all previously shipped features are intact. This suite grows with every feature — when you ship a feature, add its tests here. If any fail, flag them as Critical and investigate before proceeding with the code review.
# Run all regression suite tests
npx vitest run \
tests/db.test.ts \
tests/config.test.ts \
tests/import.test.ts \
tests/ideas.test.ts \
tests/content-types.test.ts \
tests/cli.test.ts \
tests/db-migration.test.ts \
tests/research-state.test.ts \
tests/research-sources.test.ts \
tests/research-document.test.ts \
tests/research-cli.test.ts \
tests/research-set-section.test.ts \
tests/research-project-linking.test.ts \
tests/benchmark-environment.test.ts \
tests/benchmark-state.test.ts \
tests/benchmark-results.test.ts \
tests/benchmark-companion.test.ts \
tests/benchmark-cli.test.ts \
tests/benchmark-repair-cli.test.ts \
tests/draft-frontmatter.test.ts \
tests/draft-state.test.ts \
tests/draft-benchmark-data.test.ts \
tests/draft-tags.test.ts \
tests/draft-cli.test.ts \
tests/draft-regenerate-frontmatter.test.ts \
tests/evaluate-reviewer.test.ts \
tests/evaluate-autocheck.test.ts \
tests/evaluate-synthesize.test.ts \
tests/evaluate-report.test.ts \
tests/evaluate-state.test.ts \
tests/evaluate-cli.test.ts \
tests/publish-state.test.ts \
tests/publish-pipeline.test.ts \
tests/publish-convert.test.ts \
tests/publish-table-assets.test.ts \
tests/publish-site.test.ts \
tests/publish-crosspost.test.ts \
tests/platform-images.test.ts \
tests/publish-social.test.ts \
tests/distribution-kit.test.ts \
tests/publish-distribution-kit.test.ts \
tests/publish-research-page.test.ts \
tests/publish-repo.test.ts \
tests/publish-cli.test.ts \
tests/publish-site-updates.test.ts \
tests/publish-origin-divergence.test.ts \
tests/publish-seed-on-start.test.ts \
tests/publish-verify-artifacts.test.ts \
tests/preview-gate-urls.test.ts \
tests/db-migration-v3.test.ts \
tests/update-cycles.test.ts \
tests/update-notice.test.ts \
tests/update-cli.test.ts \
tests/update-devto.test.ts \
tests/update-publish-pipeline.test.ts \
tests/publish-guard.test.ts \
tests/unpublish-state.test.ts \
tests/unpublish-cli.test.ts \
tests/unpublish-readme.test.ts \
tests/unpublish-site.test.ts \
tests/unpublish-devto.test.ts \
tests/unpublish-pipeline.test.ts \
tests/skills-crossref.test.ts \
tests/guidance-docs.test.ts \
tests/review-inventory.test.ts \
tests/update-runner-pipeline.test.ts \
tests/cross-flow-lock.test.ts \
tests/frontmatter-phase7.test.ts \
tests/origin-guard.test.ts \
tests/pipeline-registry-integrity.test.ts \
tests/paths.test.ts \
tests/cli-templates-cwd-independence.test.ts \
tests/build-bin-executable.test.ts \
tests/voice-parity.test.ts \
tests/workspace-root.test.ts \
tests/cli-json.test.ts \
tests/plan-file.test.ts \
tests/skill-smoke.test.ts \
tests/skill-fixture-integration.test.tsWhat each test covers
Phase 1 — Foundation (feature/phase-1-foundation)
| Test File | Feature | What It Validates |
tests/db.test.ts (7 tests) | SQLite schema + connection | All 8 tables created (posts, sources, benchmarks, pipeline_steps, assets, evaluations, evaluation_synthesis, metrics); user_version matches SCHEMA_VERSION; WAL mode enabled on file-backed DB; foreign keys enforced; CHECK constraints reject invalid phase values; insert/retrieve round-trip works |
tests/config.test.ts (17 tests) | .blogrc.yaml loader | Valid config parses with repo_path resolved relative to config dir; missing site.repo_path / author.name / author.github throw descriptive errors; optional sections get sensible defaults; non-existent config file throws; Phase 6 adds: site.research_dir defaults to content/posts/research when omitted; projects field accepted as optional map; projects validation rejects non-object entries; projects keys allowed to use dots (e.g., m0lz.02); distribution-kit defaults to local-card LinkedIn image generation; explicit prompt-only compatibility and invalid modes are covered |
tests/import.test.ts (5 tests) | m0lz.00 post import | Posts imported from fixture directory with correct frontmatter mapping (slug, title, phase=published, mode=imported, site_url pattern, project_id, repo_url); idempotent on re-run (INSERT OR IGNORE); throws on missing posts directory; skips posts with malformed YAML frontmatter and warns; skips posts missing required title field |
tests/ideas.test.ts (11 tests) | Editorial backlog CRUD | loadIdeas returns empty array for missing file; saveIdeas creates YAML and handles empty list; appends to existing file; priority sorting; startIdea creates DB row with phase=research and removes from YAML; startIdea throws on invalid index; removeIdea removes correct entry by priority-sorted index; removeIdea throws on invalid index; saveIdeas idempotent on identical content; startIdea honors INSERT OR IGNORE on slug collision |
tests/content-types.test.ts (6 tests) | Content type detection | Catalog project IDs (m0lz.XX) return project-launch; benchmark keywords return technical-deep-dive; generic prompts return analysis-opinion; project ID takes priority over benchmark keywords; empty prompt returns default; "test-driven development" does NOT false-positive as benchmark |
tests/cli.test.ts (15 tests) | CLI handler integration | runStatus prints formatted table and empty-state message; exits with error when DB missing; computeMetrics returns correct aggregates (posts, platforms, companion repos, evaluation pass rate); runMetrics prints output; runInit creates .blog-agent/ with all subdirs and state.db; init with --import uses config.site.content_dir; init with --import prints clean error message and sets exitCode=1 on config/repo failure; runInit idempotent on re-run; hard-fail regression (Release Prep Pass 1 Codex Finding #4): runInit throws with diagnostic mentioning "Missing shipped config template" when .blogrc.example.yaml absent from packageRoot, sets exitCode=1, does NOT create .blogrc.yaml; same for .env.example path |
Phase 2 — Research (feature/phase-2-research)
| Test File | Feature | What It Validates |
tests/db-migration.test.ts (3 tests) | Schema v1->v2 migration | Fresh DB opens at SCHEMA_VERSION=2 with unique source index; seeded v1 DB upgrades to v2 preserving data; re-opening v2 DB is idempotent |
tests/research-state.test.ts (9 tests) | Research post lifecycle | initResearchPost creates row with phase=research; idempotent re-init returns existing row unchanged; cross-phase slug collision throws; getResearchPost returns row or undefined; getResearchPost enforces phase=research boundary; advancePhase updates phase and bumps timestamp; rejects invalid phase; rejects missing slug |
tests/research-sources.test.ts (11 tests) | Source management | Inserts source with title/excerpt; deduplicates on (post_slug,url); reports existing source id; errors for missing post; detects source_type; orders by accessed_at; lists all sources; returns empty for no sources; counts correctly; returns 0 for unknown slug; rejects non-research phase posts |
tests/research-document.test.ts (18 tests) | Research documents | Writes template with all required sections; reads back losslessly; refuses overwrite without force; overwrites with force; validates missing file throws; validates all sections present; detects missing sections; detects empty sections; detects malformed frontmatter; documentPath joins correctly; YAML round-trips colons in topic; YAML round-trips quotes and hashes; validateSlug accepts kebab-case; rejects path separators; rejects uppercase/special chars; rejects empty slugs; rejects path traversal |
tests/research-cli.test.ts (13 tests) | Research CLI handlers | runResearchInit creates post+doc; refuses overwrite without --force; overwrites with --force; cross-phase safety rejects non-research slugs; rejects path traversal slugs; runResearchAddSource inserts and logs; deduplication is idempotent; missing post sets exitCode=1; runResearchShow prints fields; missing slug sets exitCode=1; runResearchFinalize fails on insufficient sources; fails on empty sections; passes when requirements met |
Phase 3 — Benchmark (feature/phase-3-benchmark)
| Test File | Feature | What It Validates |
tests/benchmark-environment.test.ts (4 tests) | Environment capture | captureEnvironment returns all required fields as non-empty strings; total_memory_gb is a positive integer; values stable across consecutive calls; formatEnvironmentMarkdown includes OS, architecture, Node.js version |
tests/benchmark-state.test.ts (17 tests) | Benchmark state lifecycle | initBenchmark transitions research→benchmark and parses targets; rejects non-research and missing posts; getBenchmarkPost returns benchmark-phase post, undefined for missing, throws for wrong phase; skipBenchmark transitions to draft with has_benchmarks=0; rejects non-research; createBenchmarkRun inserts pending row; updateBenchmarkStatus transitions pending→running→completed; listBenchmarkRuns returns ordered; completeBenchmark sets has_benchmarks=1 and advances to draft; rejects non-benchmark and missing posts; getBenchmarkRequirement routes content types correctly |
tests/benchmark-results.test.ts (11 tests) | Results storage | writeResults / readResults round-trip data; returns null for nonexistent; writeEnvironment / readEnvironment round-trip; slug validation rejects path traversal for all four functions |
tests/benchmark-companion.test.ts (6 tests) | Companion repo scaffolding | Creates src/, results/, METHODOLOGY.md, LICENSE, README.md; METHODOLOGY.md contains environment details; README.md lists targets; LICENSE contains MIT; idempotent re-scaffold preserves existing files; writeMethodology replaces all template placeholders |
tests/benchmark-cli.test.ts (19 tests) | Benchmark CLI handlers | runBenchmarkInit transitions and prints targets; rejects non-research; rejects skip content type; warns optional but proceeds for project-launch; runBenchmarkEnv captures and writes file; rejects non-benchmark; runBenchmarkRun stores results and marks completed; rejects missing environment; runBenchmarkShow displays state with run count; runBenchmarkSkip advances analysis-opinion to draft; refuses required; runBenchmarkComplete advances to draft; rejects non-benchmark; all handlers reject invalid slugs |
tests/benchmark-repair-cli.test.ts (7 tests) | Benchmark repair CLI | Repairs bad canonical results with DB-authoritative run_id; supports optional skip from benchmark or draft; preserves raw artifacts; writes repair.json; refuses required/skipped content and stale existing draft files |
Phase 4 — Draft (feature/phase-4-draft)
| Test File | Feature | What It Validates |
tests/draft-frontmatter.test.ts (32 tests) | PostFrontmatter schema | generateFrontmatter produces canonical URL, companion_repo when has_benchmarks, project from project_id, published=false, placeholder title/description; validateFrontmatter passes valid, fails missing/placeholder title/description/date/tags/published, rejects non-YYYY-MM-DD date and empty tags; serializeFrontmatter / parseFrontmatter round-trip with optional platform image fields and lifecycle fields omitted/preserved; parseFrontmatter extracts from MDX, throws on missing delimiters and invalid YAML, does not coerce "false" to true, does not mis-split on body thematic break |
tests/draft-state.test.ts (17 tests) | Draft state lifecycle | getDraftPost returns draft-phase post, throws for wrong phase, undefined for missing; initDraft creates directory structure and template MDX, is idempotent (does not overwrite valid existing draft), includes content-type-specific sections (technical-deep-dive has benchmarks, project-launch preserves benchmarks when present, analysis-opinion has analysis), throws for missing post; completeDraft advances to evaluate, rejects placeholder sections, rejects missing asset files, throws for wrong phase and missing post; registerAsset inserts and is idempotent (transactional); listAssets returns ordered list, empty for unknown slug |
tests/draft-benchmark-data.test.ts (9 tests) | Benchmark data formatting | formatBenchmarkTable produces markdown from simple key-value, handles empty data, array values as rows, nested objects flattened one level; formatMethodologyRef produces correct reference string and honors githubUser from config (no hardcoded user); getBenchmarkContext reads existing results/environment, returns nulls for missing files |
tests/draft-tags.test.ts (6 tests) | Tag taxonomy reader | readExistingTags reads tags from MDX files (flat and subdirectory-based), deduplicates, returns sorted; returns empty for missing directory, no MDX files, files without tags field |
tests/draft-cli.test.ts (33 tests) | Draft CLI handlers | runDraftInit creates draft directory and template MDX with content-type-aware sections (technical-deep-dive, analysis-opinion, project-launch), errors for wrong phase; runDraftShow prints status, errors for wrong phase, treats malformed config as best-effort; runDraftValidate fails for placeholders, passes for complete draft, fails for missing assets; runDraftAddAsset registers existing file, errors for missing file, invalid type, and path-traversal filenames; runDraftPlatformImages generates platform assets, updates frontmatter, rejects missing posts and non-draft phases without writing assets; runDraftComplete advances to evaluate, errors for wrong phase; rejects 6 invalid slug patterns |
Phase 5 — Evaluate (feature/phase-5-evaluate)
| Test File | Feature | What It Validates |
tests/evaluate-reviewer.test.ts (21 tests) | ReviewerOutput schema | normalizeText lowercases, strips punctuation, collapses whitespace; issueFingerprint is deterministic, normalizes case/whitespace, differs by reviewer, returns 12-char hex; validateReviewerOutput accepts valid outputs and empty issues, rejects missing reviewer, unknown enum, non-boolean passed, non-array issues, malformed Issue fields; parseReviewerOutput parses valid JSON, throws descriptive errors for invalid JSON and schema violations |
tests/evaluate-autocheck.test.ts (15 tests) | Deterministic structural lints | Two runs produce byte-identical output; issues sorted by (category, id); valid draft yields zero issues; missing draft returns empty list; catches frontmatter-placeholder, frontmatter-schema, placeholder-section, broken-internal-link (both /writing/ and anchor), mdx-parse unbalanced fences, benchmark-claim-unbacked for numbers not in results.json; skips benchmark-claim-unbacked silently when no results.json; catches missing-companion-repo when has_benchmarks=1 without companion_repo; does not flag when companion_repo is present; every issue tagged source=autocheck |
tests/evaluate-synthesize.test.ts (24 tests) | Two-tier issue matching + categorization | tokenize produces unique token set; jaccardSimilarity returns 1 for identical, 0 for disjoint, partial ratio for overlap; Tier 1 fingerprint clusters identical issues across reviewers, handles whitespace/case differences; Tier 2 Jaccard matches paraphrased issues >= 0.6, does not match below threshold; same-reviewer dedupes to one cluster with reviewers.size=1; categorize returns consensus/majority/single by reviewer count; two-reviewer analysis-opinion uses 2/2=consensus, 1/2=single (no majority bucket); computeVerdict is pass iff consensus=0 AND majority=0; synthesize end-to-end produces 1 consensus + 1 majority + 1 single for canonical scenario; all-empty reviewers yield pass with zero counts; JACCARD_THRESHOLD exported as 0.6 |
tests/evaluate-report.test.ts (4 tests) | Synthesis report renderer | Renders all five sections (Consensus, Majority, Single, Disagreements, Per-Reviewer Summaries); fail verdict shows FAIL badge; empty categories render as "(none)"; per-reviewer block includes model name and issue count |
tests/evaluate-state.test.ts (81 tests) | Evaluation state lifecycle | expectedReviewers routes content types to 3 vs 2 reviewers; getEvaluatePost enforces phase boundary; initEvaluation creates manifest with correct reviewers, is idempotent, promotes from draft, rejects wrong phase, purges stale reviewer artifacts when rolling a new cycle; recordReview inserts row, rejects reviewer mismatch between arg and output, rejects reviewer not in expected list for analysis-opinion, allows re-record with dedupe on listRecordedReviewers, enforces phase boundary; runSynthesis throws on missing reviewers, writes row + report on success, updates posts.evaluation_passed, throws on corrupt issues_json without writing partial row, captures MAX(id) inside the synthesis transaction (race-free pin); completeEvaluation advances on pass, throws on fail, throws without synthesis, fail-closed when synthesis pin is missing; rejectEvaluation moves back to draft with .rejected_at marker, flags subsequent records with is_update_review=1, enforces phase; readReviewerOutputFromFile validates; listRecordedReviewers / readManifest return correct shape |
tests/evaluate-cli.test.ts (24 tests) | Evaluate CLI handlers | runEvaluateInit creates 3-reviewer manifest for technical-deep-dive, 2-reviewer for analysis-opinion, promotes draft-phase posts; all 7 handlers set exitCode=1 for invalid slugs; runEvaluateAutocheck writes structural.lint.json that is byte-equal across reruns and allows open update-review cycles while posts remain published; runEvaluateRecord rejects malformed JSON with descriptive error, rejects reviewer not in expected list (methodology for analysis-opinion), rejects invalid reviewer enum; runEvaluateShow prints status table and verdict, cycle-scoped after reject+re-init (prior reviewers render as pending, old verdict not shown as current, historical cycles summarized); runEvaluateSynthesize refuses when reviewers missing, prints pass verdict after all recorded; runEvaluateComplete advances to publish on pass, exitCode=1 on fail; runEvaluateReject moves to draft (verified via direct DB query), enforces phase boundary |
Phase 6 — Publish (feature/phase-6-publish)
| Test File | Feature | What It Validates |
tests/publish-state.test.ts (43 tests) | Phase + steps-crud + lock + crash-safety helpers | getPublishPost phase boundary; initPublishFromEvaluate rejects non-evaluate phase, rejects evaluation_passed=0, idempotent when already in publish; initPublish acquires lock, advances phase, seeds 11 pipeline_steps rows via INSERT OR IGNORE; createPipelineSteps applies content-type + config pre-skips (analysis-opinion skips companion-repo + research-page, project-launch skips companion-repo, publish.devto/medium/substack=false skips corresponding steps); getNextPendingStep returns first pending OR failed in step_number order; markStepRunning/Completed/Failed/Skipped transitions work; allStepsComplete requires all 11 rows completed or skipped; reclaimStaleRunning demotes running → pending and returns count (Codex-Critical-1 regression); reconcilePipelineSteps downgrades pending/failed rows to skipped when current config disables them — operator can toggle optional destinations (publish.devto/medium/substack) between runs (Codex Pass 6 regression); does NOT upgrade skipped rows back to pending (sticky-within-cycle policy); persistPublishUrls first-writer-wins via COALESCE (Codex-Critical-2 regression); completePublishUnderLock idempotent on already-published row; runtime lock-ownership guardrail: throws when lockfile missing (Codex Pass 4 Suggestion); throws when lockfile holds different PID; completePublish gates on allStepsComplete, COALESCE URL writes; acquirePublishLock stamps PID, blocks live-holder, reclaims dead-PID stale, reclaims empty/garbage lockfile; release is idempotent |
tests/publish-pipeline.test.ts (20 tests) | runPipeline orchestrator + crash-safety regressions | Runs steps in step_number order; skips already-completed on resume; re-runs failed steps; stops on paused outcome and reverts step to pending; stops on failed outcome and records error_message; stops on thrown exception; merges urlUpdates into ctx.urls in-memory AND persists to posts row per-step via transaction (crash-safety); completes pipeline when all steps terminal, advances phase via completePublishUnderLock; does NOT advance on failure/pause; releases lock after success + failure; returns {completed, stepsRun, totalSteps, failedStep?, pausedStep?}; defensive branch when pipeline_steps table is empty; regression: reclaims stuck running step on resume and re-executes in order (Codex-Critical-1); persists URLs per-step even if process dies before completion (Codex-Critical-2); does not deadlock at completion (no release-then-reacquire); reconciles pending rows against current config so operators can disable optional destinations mid-pipeline (Codex Pass 6 regression) |
tests/publish-convert.test.ts (15 tests) | MDX → Markdown converter | Strips YAML frontmatter; removes import statements outside fences; converts image-backed JSX chart/figure components to copyable Markdown images before JSX stripping; preserves text children of block JSX; resolves relative ./assets/x and assets/x to absolute {base}/writing/{slug}/assets/x; preserves code blocks verbatim — no JSX stripping / no URL resolution inside fences, including nested backtick fences (```` vs); preserves tables, headings, lists; collapses 3+ blank lines to 2; passthrough for plain Markdown; handles empty input |
tests/publish-table-assets.test.ts (11 tests) | Portable Markdown table assets | Detects Markdown pipe tables outside fenced code; preserves fenced tables, malformed tables, blockquotes, and lists; renders deterministic assets/portable-table-<hash>.png files as readable row-card images at Medium-compatible width; wraps long cells and long tokens inside cell bounds; preserves snake_case tokens; supports public URL vs upload-placeholder reference policies; marks over-wide tables explicitly; reuses unchanged images; removes stale generator-owned table PNGs |
tests/publish-site.test.ts (21 tests) | createSitePR + checkPreviewGate (site repo PR management) | Copies draft MDX to {siteRepoPath}/{config.site.content_dir}/{slug}/index.mdx; copies assets directory recursively; copies research page to config.site.research_dir; copies complete publication bundle artifacts (distribution/manifest.json, social text, Medium/Substack paste files, portable table images) before preview; creates branch post/{slug} from main; idempotent on existing branch; commits with feat(post):; pushes/opens PR; writes pr-number.txt; checkPreviewGate polls gh pr view; all subprocess via execFileSync argv arrays; dirty-state guardrail tolerates only exact pipeline-owned paths; stages only exact current output paths plus tracked stale owned deletions; recomputes branch-dependent stale deletion staging after existing-branch checkout; rename/copy parsing rejects escapes |
tests/publish-crosspost.test.ts (40 tests) | Dev.to probe-then-create + Medium + Substack paste | mapDevToTags lowercases, strips non-alphanumerics for Dev.to alphanumeric-only tags, caps at 4, drops empty, maps dashed source tags like developer-tools -> developertools; crosspostToDevTo skips without DEVTO_API_KEY; builds POST with api-key header, canonical URL, unpublished status, tags, description, optional safe cover image; throws on unsafe covers and platform errors; probe-then-converge GETs /api/articles/me/all, PUTs existing unpublished canonical matches, rejects duplicate/published/unknown matches, normalizes trailing slash, fails closed on probe errors, paginates; Medium/Substack paste keeps image-backed MDX charts copyable, preserves fenced code blocks, naturally fits Substack subtitles without ellipses, replaces already-clipped subtitles with safe fallback copy, and fails when no safe fit exists, replaces raw Markdown tables outside fences with upload-checklist placeholders, and writes platform-specific table upload checklists with canonical URL, local paths, reference-only public URLs, alt text, and captions |
tests/platform-images.test.ts (22 tests) | Deterministic platform image generation | ensurePlatformImages emits exact Dev.to 1000x420 PNG, Medium 1200x675 PNG, and Substack preview 1200x630 PNG from the shared fallback SVG article-card framework; upgrades legacy generated devto-cover.webp defaults while preserving custom WebP covers; refreshes generator-owned defaults in draft phase but keeps them read-only during publish verification; validates and preserves explicit platform assets including URLs; rejects unsafe paths before writes/receipts/frontmatter rewrites; keeps fallback labels config-derived; throws on wrong dimensions; proves stable filenames with no numbered duplicates |
tests/publish-social.test.ts (17 tests) | LinkedIn + Hacker News text generation | containsEmoji detects U+1F300-1FAFF and U+2600-27BF ranges; generateLinkedIn fills a public-only pinned shape with hook, body, canonical URL, hashtags, and optional timing; rejects internal image prompt/alt/path labels, emojis, and abrupt ellipsis clipping; replaces already-clipped descriptions with safe fallback copy; writes to {socialDir}/{slug}/linkedin.md; generateHackerNews title ≤80 chars (title truncation only), Show HN: prefix ONLY for project-launch, no prefix for technical-deep-dive / analysis-opinion; fits first-comment descriptions without abrupt ellipses; includes canonical_url and repo link only when real; throws on emoji; writes hackernews.md; creates output dir |
tests/distribution-kit.test.ts (28 tests) | Durable distribution-kit generation | Metadata precedence by source mode (publish/update/backfill); hub MDX/frontmatter wins for published backfill; update cycle metadata is carried without invented summaries; prompt-only writes prompt/manifest without provider calls while public LinkedIn copy stays artifact-free and legacy OpenAI-null-image manifests still load; local-card writes a 1200x1200 assets/linkedin-feed.png without provider calls, records local-card provenance, and refuses symlink output paths; local-card SVG output escapes and bounds long text while avoiding desk/workspace motifs; prompt quality guards reject desk/workspace motifs without rejecting metadata that mentions release notes or laptops; Medium/Substack paste files, upload checklists, and portable table assets are manifest-tracked and hash-verified; social-disabled configs still emit publication paste artifacts when cross-post platforms are enabled; disabled platforms stay null per individual destination; generated mode records GPT Image 2 provenance and reuses matching manifests byte-for-byte; manifest reuse compares current expected artifact hashes instead of trusting rewritten manifest hashes; loadDistributionKit refuses tampered text/checklist/image/table bytes, unsafe manifest paths, symlinks, and unsafe reused manifests; required mode fails without OPENAI_API_KEY; product IDs like m0lz.01 are not truncated and lead-sentence helpers throw instead of hard-clipping |
tests/publish-distribution-kit.test.ts (27 tests) | Pre-site generation + strict site persistence | createSitePR required-image mode and no-safe-fit Substack subtitles fail before checkout/copy/commit/push/PR; the real social-text registry step reuses a valid manifest-derived kit including Medium/Substack paste files, upload checklists, table assets, and optional assets/linkedin-feed.png without provider/fetch calls, and regenerates self-referential tampered text before site persistence; paste steps verify existing manifest artifacts without regenerating paste/table assets; disabled paste steps skip without requiring bundle files; persistDistributionKitToSite stages only owned distribution paths and exact optional assets, commits chore(distribution): {slug}, removes tracked stale generator-owned text, LinkedIn image, and portable-table artifacts when channels/images/tables are disabled or regenerated, enforces origin and staged-index guards, refuses unrelated dirty site state, conflicting reviewed artifacts, and unexpected ahead commits before staging, returns exact no-op when current, pushes exact crash-replay ahead commits including already-committed stale artifact deletions, and refuses wrong-subject/wrong-file ahead commits |
tests/publish-research-page.test.ts (15 tests) | Research page MDX generator | Generates from research doc + benchmark results; includes thesis, key findings, bibliography; cross-links to post canonical URL and companion repo URL; skips for analysis-opinion when research doc absent with clear reason; generates for analysis-opinion WHEN research doc present; fills template placeholders ({{title}}, {{thesis}}, etc.); reads description from draft MDX frontmatter, not from posts table (schema has no description column); idempotent (second run overwrites consistently); throws when template file missing; throws when post not in DB |
tests/publish-repo.test.ts (21 tests) | Companion repo probe-then-create + origin-URL guardrail | Skips for analysis-opinion with reason "No companion repo for analysis-opinion"; skips for project-launch with reason "Existing project repo"; for technical-deep-dive: probes with gh repo view, success → ensures remote and pushes to main; missing → runs gh repo create --public --source=. --push with description; race fallback on "already exists" stderr → adds remote + pushes; builds repo name as {config.author.github}/{slug} (not hardcoded); returns repo_url for frontmatter propagation; handles reposDir/{slug} missing as skip; subprocess via execFileSync; origin-URL guardrail (Codex Pass 5 Critical regression): throws when origin points at a different GitHub repo (SSH or HTTPS form), throws for non-GitHub remotes, accepts when owner/name match; race-fallback branch enforces the same guardrail; parseGitHubRemoteUrl helper validated across SSH-with/without-.git, HTTPS-with/without-.git, non-GitHub hosts, garbage input |
tests/publish-cli.test.ts (20 tests) | Publish CLI handlers (blog publish start / show / distribution-kit / platform-images / reopen-draft) | Invalid slug → exitCode=1 before DB open; post not found → descriptive error; wrong phase (e.g., research) → exitCode=1 with valid-phases hint; evaluate phase → promotes + seeds 11 steps + runs pipeline; publish phase → resumes without duplicating rows; runner completed:true → success log; pausedStep → info log (NOT error); failedStep → exitCode=1; runner throws → "pipeline crashed" message; runPublishShow prints slug/phase/step table; empty table prints "No pipeline steps yet"; malformed config is best-effort (show still prints); ctx.urls hydrated from posts row URL columns before runPipeline (Codex Pass 2 regression) — resumed runs see prior-invocation URLs; fresh publish hydrates to empty object; blog publish distribution-kit backfills local artifacts for published posts; published posts can regenerate platform images; reopen-draft handles failed pre-site recovery |
tests/publish-site-updates.test.ts (20 tests) | updateFrontmatter + updateProjectReadme (direct-push steps 9 & 10) | updateFrontmatter: switches to main BEFORE any file mutation (Codex Pass 4 Critical regression — prior ordering made checkout fail on stale local main); writes published:true, canonical, devto_url, companion_repo to site MDX frontmatter; commits with chore(post): prefix; pushes to origin main; uses execFileSync with argv arrays; idempotent no-op when nothing staged AND HEAD not ahead of origin; crash-replay push-ahead uses inspectAheadCommits with strict match: requires exactly 1 ahead commit, subject == chore(post): {slug} add platform URLs, touched file == {content_dir}/{slug}/index.mdx — throws on wrong subject, wrong file, or 2+ commits ahead (Codex Pass 4 High regression prevents shipping operator's unrelated local work); throws on missing site repo; throws on missing MDX; index-cleanness guardrail via assertIndexClean: refuses to proceed when operator has staged unrelated files — otherwise the subsequent git commit would sweep them into the chore(post): commit and push to origin/main (Codex Pass 6 regression); updateProjectReadme: resolves config.projects[id] against dirname(configPath) (relative path works); switches to main before file mutation; same index-cleanness guardrail on project repo; inserts link under ## Writing heading; creates heading when absent; commits with chore: prefix (not docs:) per contract; skips when no project_id, when no projects config entry, when project dir missing; idempotent when canonical URL already in README; crash-replay push-ahead uses same strict match; refuses to push unrelated local README commits; throws on missing post |
Phase 7 — Lifecycle (feature/phase-7-lifecycle)
| Test File | Feature | What It Validates | |
tests/db-migration-v3.test.ts (12 tests) | Schema v2→v3 canonical table-rebuild | Fresh DB opens at SCHEMA_VERSION=3; v1→v3 migration preserves pipeline_steps rows with cycle_id=0 via rename→create→INSERT..SELECT→drop; v2→v3 preserves all pre-v3 data with cycle_id=0; update_cycles + unpublish_steps created additively; partial unique index idx_update_cycles_open rejects a second INSERT when closed_at IS NULL; multiple closed cycles per post allowed; re-opening a v3 DB is a no-op; FK enforcement preserved through table rebuild | |
tests/update-cycles.test.ts (17 tests) | Update-cycle lifecycle | openUpdateCycle validates phase=='published', enforces single-open invariant, writes update_cycles row + update_opened metric in one transaction; rejects duplicate open with actionable error; closeUpdateCycle sets closed_at + ended_reason, writes update_aborted OR update_completed metric; getOpenUpdateCycle returns row or null; listUpdateCycles returns ordered by cycle_number | |
tests/update-notice.test.ts (13 tests) | Cycle-keyed update-notice MDX marker | Marker regex update-notice cycle=(\d+) date=(\d{4}-\d{2}-\d{2}); same-cycle re-run replaces in-place regardless of date; multi-cycle preserves historical blocks; atomic temp+rename writes; no date-based matching inlined anywhere | |
tests/update-cli.test.ts (7 tests) | blog update abort / start / show CLI | abort closes cycle + writes metric + PRESERVES .blog-agent/drafts/{slug}/ artifacts on disk; exit 1 when no open cycle; exit 1 on invalid slug before DB open; start refuses with --summary "" when require_summary=true; show prints phase/update_count/cycle table | |
tests/update-devto.test.ts (4 tests) | updateDevToArticle probe-then-PUT with POST fallthrough | Probe match → PUT body contains title + body_markdown + canonical_url + tags + description; probe miss → falls through to POST (recovers from manual Dev.to deletion); PUT 500 throws; PUT 422 throws with validation context | |
tests/update-publish-pipeline.test.ts (5 tests) | createSiteUpdate + completeUpdateUnderLock unit coverage | site-update uses update/<slug>-cycle-<N> branch, chore(site): update <slug> (cycle <N>) commit, Update <title> (cycle <N>) PR title; MDX body lands in site repo (not just frontmatter); completeUpdateUnderLock closes cycle, increments update_count, sets last_updated_at, phase stays published, writes update_completed metric with value=cycleId; grep invariant: update finalizer does NOT advancePhase; throws when no open cycle | |
tests/publish-guard.test.ts (2 tests) | blog publish start refuses on open update cycle | getOpenUpdateCycle check runs early; exit 1 with message mentioning BOTH blog update publish and blog update abort; no pipeline_steps rows created | |
tests/unpublish-state.test.ts (6 tests) | Unpublish init + finalize state | initUnpublish seeds all 7 step rows + writes unpublish_started metric; pre-skips rows disabled by config; returns alreadyUnpublished:true for already-unpublished post; rejects non-published phase with error message including current phase; rejects missing posts; completeUnpublishUnderLock advances phase + sets unpublished_at + writes unpublished metric via finalizePipelineUnderLock | |
tests/unpublish-cli.test.ts (3 tests) | `blog unpublish start | show` CLI | --confirm required: missing flag → exit 1 + zero DB writes + zero fetch calls (fetch-spy asserted); rejects non-published posts with phase-boundary error; show prints status + "No unpublish steps recorded" when not started |
tests/unpublish-readme.test.ts (5 tests) | revertProjectReadmeLink skip paths + guardrails | Three skip paths: no project_id, config.projects[id] absent, link not in README; assertIndexClean throws when staged unrelated file exists; requireOriginMatch throws on wrong-origin remote (real git repos, not mocked) | |
tests/unpublish-site.test.ts (5 tests) | createSiteRevertPR PR-only trust boundaries | Happy-path: unpublish/<slug> branch, MDX flipped to published: false, PR opened via gh; idempotent on existing PR; requireOriginMatch throws on wrong origin (no push, no PR); throws on unparseable GitHub URL; grep-verified: source file contains no push origin main and no site_revert_mode | |
tests/unpublish-devto.test.ts (7 tests) | unpublishFromDevTo Forem PUT | Skips when DEVTO_API_KEY unset; probe match → PUT body is exactly { article: { published: false } } per spike; probe miss → skip with reason; probe HTTP 500 throws; PUT 500 throws; PUT 422 throws; probe pagination stops on short page | |
tests/unpublish-pipeline.test.ts (4 tests) | runUnpublishPipeline E2E via mocked step modules | Happy path drives all 7 steps → phase=unpublished, unpublished_at set, metrics=[unpublish_started, unpublished]; per-slug lock contention via acquirePublishLock direct; runner imports from ../publish/lock.js (trust-boundary proof); failure path leaves phase=published and records failedStep | |
tests/skills-crossref.test.ts (2 tests) | Orchestrator skill files contract | All three skill files (blog-pipeline, blog-update, blog-unpublish) contain exactly H2 set {Preflight, Workflow, CLI Reference, Troubleshooting, Degraded Mode}; every blog <cmd> in fenced code blocks resolves to a registered Commander handler in src/cli/index.ts (AST-based parse, not string grep) | |
tests/guidance-docs.test.ts (8 tests) | Command/rule guidance tripwires | Review command docs must describe Dev.to tags as alphanumeric-only and never preserve punctuation; evaluate/plan-feature docs must treat tenant-policy data-exfiltration denials as policy-blocked with no Responses API fallback; self-heal docs must select contract-bearing source plans before proof/evidence artifacts; Codex source-command review docs must reference the real .claude-plugin surface, not absent Codex plugin paths; .claude and .codex drafting rules must agree on the PostFrontmatter field list including devto_main_image and lifecycle fields; publish guidance captures complete-bundle and portable-table invariants | |
tests/review-inventory.test.ts (3 tests) | Review regression inventory tripwire | Asserts .agents/skills/source-command-review/SKILL.md, .claude/commands/review.md, and .windsurf/workflows/review.md mention every current tests/*.test.ts file, so newly added tripwire tests cannot be silently omitted from the cumulative review command or its mirrored review workflow. | |
tests/update-runner-pipeline.test.ts (3 tests) | TRUE E2E update-mode runPipeline via step registry mock | Every UPDATE_STEP_NAMES step runs in order via real runPipeline({publishMode:'update', cycleId}); cycle_id persists on every pipeline_steps row (no cycle_id=0 leakage); post.phase stays 'published'; update_count increments; update_completed metric with value=cycleId; pause/resume works in update mode | |
tests/cross-flow-lock.test.ts (3 tests) | Cross-flow lock mutual exclusion | TRUE same-slug contention: runPipeline(alpha) blocks on step 2 → lockfile contains process.pid → runUnpublishPipeline(alpha, lockTimeoutMs=100) throws with contention error (real runner, not primitive); release-then-reacquire succeeds; disjoint slugs don't contend: publish(alpha) + unpublish(beta) both complete concurrently | |
tests/frontmatter-phase7.test.ts (15 tests) | PostFrontmatter Phase 7 round-trip + cross-repo fixtures | serialize + parse preserves unpublished_at / updated_at / update_count (numeric); emits all three together; omits when unset (legacy compat); parses legacy MDX with new fields undefined; parses production-shape Phase 7 MDX with all platform + lifecycle fields; tolerates update_count as YAML string "5"; reads 6 fixture files from fixtures/frontmatter-phase7/ asserting parsed shapes; drift guard: every .mdx on disk must appear in REGISTERED_FIXTURES and vice versa | |
tests/origin-guard.test.ts (16 tests) | Split tolerant/strict origin APIs | parseGitHubRemoteUrl handles SSH/HTTPS; returns null for non-GitHub; getOriginState returns 'matches'/'absent'; narrows catch to 'No such remote' stderr only (re-throws environment errors); throws on wrong-target or unparseable URL; normalizes case (GitHub is case-insensitive); forces LC_ALL=C/LANG=C on subprocess (locale independence); requireOriginMatch throws on absent (unlike tolerant variant); expectedSiteCoords prefers config.site.github_repo over basename(repo_path) fallback; handles trailing slash in repo_path | |
tests/pipeline-registry-integrity.test.ts (8 tests) | Registry ↔ step-tuple invariants | PIPELINE_STEPS exports every PUBLISH_STEP_NAMES entry; every UPDATE_STEP_NAMES entry; pairwise unique names; numbers cover 1..11 with slot 3 shared between site-pr and site-update; each name maps to consistent number; every step has callable execute; no step name outside union of publish+update tuples; createPipelineSteps seeds DB step_numbers that match tuple positions per mode (the ACTUAL invariant the runner depends on) |
Release Prep (feature/release-prep-v0-1)
Ships the v0.1.0-readiness bundle: a shared package-root resolver that fixes a latent CWD-relative template-loading bug, a four-layer packaging gate (verify-pack.mjs), clean build (rm -rf dist && tsc), adversarially-reviewed release runbook, and CI. Validated against a Tier-2 contract with seven adversarial passes (1 Claude evaluator + 6 Codex GPT-5.5 high passes — final verdict clean / approve).
| Test File | Feature | What It Validates | |||
tests/paths.test.ts (4 tests) | Shared package-root resolver | findPackageRoot(import.meta.url) from the test file returns a real path ending in /m0lz.01; synthetic src-layout URL (file:///tmp/fakeProj/src/cli/foo.ts) walks up to seeded package.json; synthetic dist-layout URL (file:///tmp/fakeProj/dist/cli/foo.js) resolves identically (proving no offset-arithmetic brittleness between src/ and dist/ layouts); missing-package.json throws with the URL in the error message (uses a path whose ancestors definitionally have none) | |||
tests/cli-templates-cwd-independence.test.ts (1 test) | CLI works from arbitrary CWD | beforeAll unconditionally rebuilds dist (Release Prep Pass 1 Codex Finding #1: without this, a stale dist could silently false-positive); spawnSync('node', [distPath, 'init'], { cwd: mkdtempSync }) from an empty OS-tmpdir (macOS /var/folders/..., physically outside repo ancestry so findPackageRoot cannot accidentally resolve against the real package.json); asserts exit 0, .blog-agent/state.db created, AND byte-equal comparison of .blogrc.yaml ↔ shipped .blogrc.example.yaml + .env ↔ shipped .env.example (proves both the templates/ helper fix AND the init.ts hard-fail path) | |||
tests/build-bin-executable.test.ts (1 test) | Build preserves +x on CLI entrypoint | tsc strips the owner-execute bit on every rebuild; on initial npm install / npm link, npm applies +x once but subsequent npm run build regenerates dist/cli/index.js as 0644, causing /bin/sh: blog: Permission denied inside /blog skill exec fences. scripts/chmod-bin.mjs runs as the last step of npm run build; this test statSyncs the built entry after vitest globalSetup and asserts mode & 0o100 === 0o100 (owner-executable). Catches any future refactor that drops the chmod step before the next contributor hits it in a skill session. | |||
tests/research-set-section.test.ts (13 tests) | blog research set-section CLI + library | setResearchSection(researchDir, slug, section, content) replaces one of the 7 keyed sections (thesis, findings, sources_list, data_points, open_questions, benchmark_targets, repo_scope) while preserving frontmatter and all other sections; invalid section key rejected with enumerated valid keys in error; path-traversal slug rejected at library layer via validateSlug; missing-doc throw with actionable "run blog research init first" hint; overwrite semantics (replace, not append); populating all 7 sections satisfies the document-gate half of finalize. CLI handler: --content inline prose path; --from-file <path> reads file and injects; mutual-exclusion (both flags → exitCode=1); empty-content rejected; non-research phase rejected; missing --from-file target rejected; path-traversal slug rejected at CLI boundary before DB open. Enables the skill-driven research-authoring path (hash-bound section prose travels through the plan) so the skill never needs Write/Edit scope to complete the finalize gate. | |||
tests/voice-parity.test.ts (4 tests) | Voice rules byte-parity between dev rule and plugin skill | .claude/rules/voice.md (dev-facing, path-scoped auto-load) and .claude-plugin/skills/blog/VOICE.md (shipped with plugin tarball, loaded by /blog skill when composing set-section prose) must stay byte-identical below the frontmatter boundary. stripFrontmatter regex extracts body after closing ---\n; parity assertion catches any drift between the two sources. Dev rule must have paths: frontmatter; plugin file must have NO frontmatter (plugin skills don't path-scope); plugin file must not contain `jmolz | m0lz.dev | @molz | DEVTO_API_KEY=` identity strings (skill-content hygiene, duplicated here so a failure points at the right file). Prevents "voice rule updated in one place, other location drifts" silent divergence. |
/blog Skill Plugin (feature/blog-skill-plugin)
Ships the /blog Claude Code plugin + blog agent CLI subcommand family. Single skill with SHA256-bound plan artifacts as the structural safety boundary; destructive execution only via blog agent apply.
| Test File | Feature | What It Validates |
tests/workspace-root.test.ts (11 tests) | Workspace-root detection | findWorkspaceRoot ancestor-walks from cwd to .blog-agent/state.db; honors override (--workspace flag) and envVar (BLOG_WORKSPACE); override beats envVar beats walk; throws WorkspaceNotFoundError with actionable hints (--workspace + BLOG_WORKSPACE cited); resolveUserPath preserves _BLOG_ORIGINAL_CWD for relative paths, passes absolutes through unchanged |
tests/cli-json.test.ts (6 tests) | --json envelope surface | Every --json output has schema_version='1' + versioned kind + ISO generated_at + typed data; WorkspaceStatus / PublishPipeline / UpdatePipeline / UnpublishPipeline / EvaluationState shapes; empty-DB graceful fallback for blog status --json |
tests/plan-file.test.ts (64 tests) | Plan schema + SHA256 hash + validator + apply runner + receipt + apply-lock | canonicalPlanJSON is key-order independent and drops approved_at/payload_hash; computePlanHash deterministic; schema rejects unknown content_type (park-research rejected), unknown depth, abstract step commands (run-panel rejected), bare "research finalize" without blog prefix, empty steps/venues, blog agent * nesting, flag-like tokens inside command (e.g. "blog status --sneak"), BANNED_ARG_FLAGS in args (--workspace, --help, -h, --version, -V + compact = forms), slug traversal at the shared-schema layer ("../../outside"), namespace-only commands ("blog research" with args=[finalize, beta]), whitespace variants (double-space, tab, newline, leading space), workspace-global mutators ("blog init", "blog ideas add/start/remove"), args[0] != plan.slug for slug-bearing commands; accepts blog status/metrics as read-only leaves (not "blog ideas" which has Commander subcommands via default-action); validatePlanForApply throws NO_APPROVAL/HASH_MISMATCH/WORKSPACE_MISMATCH/SCHEMA_INVALID/CRASH_RECOVERY_REQUIRED; DB-authoritative skip state (schema v4 agent_plan_runs + agent_plan_steps); applyPlan writes receipt with per-step exit_code/stdout_tail/stderr_tail/duration_ms, tampered receipt JSON is a no-op (DB authoritative — forged-skip vector closed), RECEIPT_CONFLICT derived from DB open runs not receipt file (forgery-resistant), RECEIPT_HASH_MISMATCH on same-plan-id re-approved with different content, --restart clears ALL open runs for slug (not just current plan_id), CRASH_RECOVERY_REQUIRED on attempt-sentinel-without-completion; slug-scoped apply lock serializes different plan_ids for same slug, reclaims legacy bare-PID stale lockfiles, honest PID-liveness policy (no false PID-reuse hardening); stepToArgv strips "blog" prefix + concatenates command words with args |
tests/skill-smoke.test.ts (25 tests) | Plugin static shape + SKILL.md discipline + sibling-doc parity + install-docs structural contract | plugin.json declares blog skill at skills/blog; SKILL.md frontmatter parses + allowed-tools contains Bash(blog:) AND explicitly excludes Write/Edit/Bash(gh:) (CLI-enforced gate, not prose-gated); body ≤ 200 lines; no node -e / cat / head in code fences; every read command uses --json; no bare destructive execs outside blog agent apply; no hardcoded identity values (jmolz/m0lz.dev/DEVTO_API_KEY=); sibling docs (JOURNEYS.md + CHECKPOINTS.md) enforce the same discipline; skill-content identity hygiene scans .claude-plugin/skills/** recursively with manifest-field whitelist for plugin.json author/homepage; install-docs structural contract: npm-bundled path ships all required files, plugin.json declares readable SKILL.md, install docs reference files that actually exist; contributor symlink resolves; REFERENCES/JOURNEYS/CHECKPOINTS exist; .claude-plugin/** in package.json#files |
tests/skill-fixture-integration.test.ts (27 tests) | Real skill-to-CLI handoff end-to-end + crash recovery + workspace pinning | beforeAll unconditionally rebuilds dist; spawnSync(node, [distPath, ...]) drives the real CLI: preflight → plan → verify-unapproved (exit 2 NO_APPROVAL) → approve (atomic approved_at + payload_hash) → verify-approved (exit 0) → apply (receipt written with plan_id pin, plan_payload_hash binding, step exit/stdout/stderr tail, completed_at, overall_exit=0); tamper plan → verify exits 2 HASH_MISMATCH; cross-workspace plan → verify exits 2 WORKSPACE_MISMATCH; abstract command via --steps-json → plan exits 2 SCHEMA_INVALID; blog agent nesting in --steps-inline → plan exits 2 SCHEMA_INVALID; --steps-inline writes a plan whose steps exactly match the payload; --steps-inline + --steps-json → exits 2 (mutually exclusive); slug traversal + leaf-level symlink rejected; --workspace smuggled through step args rejected; --output path clamp (outside plans/ reject, wrong ext reject, valid accept); hand-authored traversal slug rejected at verify; completed prior plan does NOT block a later plan for same slug; RECEIPT_CONFLICT cannot be forced by forged receipt (DB authority only); stale open-run recovery via --restart clears debt across plan_ids; RECEIPT_HASH_MISMATCH on re-approval with different content; receipt-JSON tampering is benign (DB authoritative); apply is resumable (no-op when complete); preflight respects --workspace override from outside the workspace; preflight honors --workspace > BLOG_WORKSPACE precedence; apply pins spawned children to plan.workspace_root (BLOG_WORKSPACE cannot redirect child via child env scrub + --workspace prepend); --workspace=/path compact form recognized; empty --workspace= rejected with explicit error; startup shim --workspace operand excluded from positional walk so agent preflight classification holds |
v0.3 Dogfood Hardening (feature/v0.3.0-dogfood-hardening)
Five connected fixes surfaced by the first live dogfood run of blog publish: project-launch posts now require a resolvable project ID at research-init, generateFrontmatter resolves companion_repo from config.projects[id] + git origin, createSitePR refuses when local main is ahead of origin/main, runPublishStart always seeds pipeline_steps, the preview-gate surfaces canonical/supplementary/companion URLs in its JSON envelope, and a new blog draft regenerate-frontmatter recovery command rewrites the local draft from the current post+config. See CHANGELOG.md § 0.3.0 for the full accounting.
| Test File | Feature | What It Validates |
tests/research-project-linking.test.ts (15 tests) | --project flag + prompt-regex fallback + library-level PROJECT_UNLINKED guard | extractProjectIdFromPrompt matches m0lz.01 / project.42 / repo.7 and returns lowercased; returns null for bare decimals (3.14), prose without catalog IDs, and leading-digit tokens (2.0); first-match wins on multi-ID prompts. initResearchPost stores project_id when provided, leaves it null for non-project-launch types, throws [AGENT_ERROR] PROJECT_UNLINKED when project-launch lacks a projectId (including empty string + null); runResearchInit resolves projectId via --project flag → prompt regex → null, classifies m0lz.N IDs as project-launch via CATALOG_PATTERN, does NOT auto-classify generic catalog IDs like repo.7, exits 1 with PROJECT_UNLINKED when project-launch has no resolvable ID and inserts no row. |
tests/publish-origin-divergence.test.ts (6 tests) | assertOriginInSync + site-pr --allow-main-ahead override | Self-contained origin + clone bare-repo fixture. Passes silently when local main matches origin/main; throws [AGENT_ERROR] ORIGIN_OUT_OF_SYNC when local main is ahead (exact commit count reported for 1 and 3-commit lead); throws ORIGIN_OUT_OF_SYNC when the branch does not exist on origin; re-throws environment errors with context. createSitePR(..., { allowMainAhead: true }) is exercised directly with mocked subprocesses, verifying the origin-sync assertion is skipped and the bypass warning is emitted. |
tests/publish-seed-on-start.test.ts (4 tests) | runPublishStart always seeds pipeline_steps on phase=publish | Post driven to phase=publish WITHOUT running blog evaluate complete's seeding path has 0 rows in pipeline_steps; calling runPublishStart seeds 11 rows and logs seeded 11 pipeline_steps for <slug>; re-running is idempotent (no duplicate rows, no fresh seed log); phase=evaluate path still promotes + seeds correctly; phase=published rejected with actionable "already published" error pointing at blog update start or blog unpublish start --confirm. |
tests/publish-verify-artifacts.test.ts (1 test) | Publish verify pins evaluated artifacts | Mutates .blog-agent/drafts/{slug}/index.mdx after completeEvaluation; real PIPELINE_STEPS.verify succeeds before the mutation and fails after it with the blog publish reopen-draft ... --reason "evaluated artifact drift" recovery command before any site-pr mutation can run. |
tests/preview-gate-urls.test.ts (9 tests) | computePreviewUrls + blog publish show --json envelope | canonicalUrl always emitted from config.site.base_url + slug; supplementaryUrl null when no research-pages/<slug>/index.mdx, populated otherwise; companionRepoUrl resolves from config.projects[post.project_id] + git origin when post.repo_url is empty, prefers post.repo_url when set, null when neither project_id nor repo_url available, null when project dir has no origin. blog publish show <slug> --json envelope includes data.preview_urls.{canonicalUrl, supplementaryUrl, companionRepoUrl} with the correct values. |
tests/draft-regenerate-frontmatter.test.ts (9 tests) | blog draft regenerate-frontmatter recovery command | Adds missing companion_repo from config.projects + git origin, preserving body (including body with thematic-break ---); --project <id> repairs stale project-launch rows with project_id=NULL; missing project IDs now fail with PROJECT_UNLINKED instead of silently no-oping; writes .frontmatter-regenerated.json receipt with previous/new SHA256 hashes + fields_changed; preserves operator-authored title/description/tags; rejects phase=published without touching the file; fails loudly when draft MDX missing; leaves siblings untouched; rejects invalid slugs. |
Source files these tests protect
src/core/db/schema.ts— SQL schema for all 8 tables, SCHEMA_VERSION constantsrc/core/db/database.ts—getDatabase,closeDatabase, migration logic, WAL + foreign keyssrc/core/db/types.ts— Row interfaces, ContentType/Phase/Mode/etc. enumssrc/core/config/loader.ts—loadConfig,validateConfig, path resolution, defaultssrc/core/config/types.ts— BlogConfig and subsection interfacessrc/core/migrate/import-posts.ts—importPostswith frontmatter parsing and content_dir parametersrc/core/draft/content-types.ts—detectContentType, BENCHMARK_KEYWORDS listsrc/cli/index.ts— Commander.js entry point, command registrationsrc/cli/init.ts—runInitwith injectable baseDir, subdirectory + DB + template copysrc/cli/status.ts—runStatuswith formatted table outputsrc/cli/metrics.ts—runMetrics,computeMetricswith aggregate queriessrc/cli/ideas.ts—addIdea/listIdeas/startIdea/removeIdea, YAML persistencesrc/cli/research.ts—runResearchInit/runResearchAddSource/runResearchShow/runResearchFinalize/runResearchSetSection, slug validation, phase boundarysrc/core/research/state.ts—initResearchPost,getResearchPost,advancePhase, phase boundary enforcementsrc/core/research/sources.ts—addSource,listSources,countSources, phase boundary enforcementsrc/core/research/document.ts—writeResearchDocument,readResearchDocument,validateResearchDocument,setResearchSection+SECTION_KEYSconst,validateSlug, path traversal guardsrc/cli/benchmark.ts—runBenchmarkInit/runBenchmarkEnv/runBenchmarkRun/runBenchmarkShow/runBenchmarkSkip/runBenchmarkComplete/runBenchmarkRepair,BenchmarkPaths, slug validation, phase boundarysrc/core/benchmark/environment.ts—captureEnvironment,formatEnvironmentMarkdownsrc/core/benchmark/state.ts—initBenchmark,getBenchmarkPost,skipBenchmark,completeBenchmark,repairBenchmarkResults,repairSkipOptionalBenchmark,createBenchmarkRun,updateBenchmarkStatus,listBenchmarkRuns,getBenchmarkRequirementsrc/core/benchmark/results.ts— input/canonical result parsing,canonicalizeBenchmarkResults,writeResults,readResults,readBenchmarkSummary,writeEnvironment,readEnvironment, slug validationsrc/core/benchmark/companion.ts—scaffoldCompanionRepo,writeMethodology, template renderingtemplates/benchmark/methodology.md— METHODOLOGY.md scaffold templatesrc/cli/draft.ts—runDraftInit/runDraftShow/runDraftValidate/runDraftAddAsset/runDraftComplete,DraftPaths, slug validation, phase boundarysrc/core/draft/frontmatter.ts—PostFrontmatterschema,generateFrontmatter,validateFrontmatter,serializeFrontmatter,parseFrontmattersrc/core/draft/state.ts—getDraftPost,initDraft,completeDraft,registerAsset,listAssets,PLACEHOLDER_PATTERNsrc/core/draft/benchmark-data.ts—formatBenchmarkTable,formatMethodologyRef,getBenchmarkContext(config-drivengithubUser)src/core/draft/tags.ts—readExistingTagswith graceful fallbacksrc/core/draft/template.ts—renderDraftTemplate, content-type-aware sectionssrc/cli/evaluate.ts—runEvaluateInit/runEvaluateAutocheck/runEvaluateRecord/runEvaluateShow/runEvaluateSynthesize/runEvaluateComplete/runEvaluateReject,EvaluatePaths, slug validation, phase boundarysrc/core/evaluate/reviewer.ts—Issue/ReviewerOutputtypes,normalizeText,issueFingerprint,validateReviewerOutput,parseReviewerOutputsrc/core/evaluate/autocheck.ts—runStructuralAutocheck(deterministic lints: frontmatter, placeholders, broken links, MDX parse, benchmark claims, companion repo)src/core/evaluate/synthesize.ts—tokenize,jaccardSimilarity,matchIssues,categorize,computeVerdict,synthesize,JACCARD_THRESHOLDsrc/core/evaluate/report.ts—renderSynthesisReportsrc/core/evaluate/state.ts—getEvaluatePost,expectedReviewers,initEvaluation,recordReview,runSynthesis,completeEvaluation,rejectEvaluation, phase boundary enforcementskills/blog-evaluate.md— three-reviewer workflow, Codex CLI invocation templates, degraded-mode fallback.claude/commands/evaluate.md,.claude/commands/plan-feature.md,.claude/commands/review.md,.agents/skills/source-command-evaluate/SKILL.md,.agents/skills/source-command-plan-feature/SKILL.md,.agents/skills/source-command-review/SKILL.md,.windsurf/workflows/review.md— command guidance protected bytests/guidance-docs.test.ts.claude/rules/drafting.md,.codex/rules/drafting.md,.claude/rules/lifecycle.md,.codex/rules/lifecycle.md,.claude/rules/release.md,.codex/rules/release.md,.codex/rules/testing.md— cross-surface rule guidance for frontmatter, Dev.to API contracts, fallback keys, and review-suite registrationsrc/cli/publish.ts—runPublishStart/runPublishShow,PublishCliPaths, slug validation, phase dispatch (evaluate→promote, publish→resume), ctx.urls hydration from posts rowsrc/core/publish/types.ts—PUBLISH_STEP_NAMES11-tuple (authoritative ordering),PublishStepName/PublishStepStatusenums,PipelineStepRow,PublishUrls,PublishPaths,PostRowre-exportsrc/core/publish/lock.ts—acquirePublishLock(O_CREAT|O_EXCL + PID stamp, stale-PID reclaim viaprocess.kill(pid,0), empty/garbage lockfile reclaim, Atomics.wait spin, release is idempotent)src/core/publish/phase.ts—getPublishPost,initPublishFromEvaluate,initPublish,completePublish,completePublishUnderLock(no lock re-entry, idempotent onpublished, runtime lock-ownership guardrail: reads lockfile + verifies PID matches process.pid),persistPublishUrls(COALESCE first-writer-wins)src/core/publish/steps-crud.ts—createPipelineSteps(content-type + config pre-skips),getNextPendingStep,markStepRunning/Completed/Failed/Skipped,getPipelineSteps,allStepsComplete,reclaimStaleRunning(crash-safety),reconcilePipelineSteps(resume-time re-application of config-driven skips so optional destinations can be disabled between runs)src/core/publish/convert.ts—mdxToMarkdown,stripFrontmatter,removeImports,removeJsxComponents,resolveAssetUrls, fence-aware state machine with nested-backtick handlingsrc/core/publish/table-assets.ts— deterministic Markdown table detection, portable table PNG rendering, stale generated table cleanup, manifest metadatasrc/core/publish/platform-copy.ts— natural platform copy fitting, abrupt-ellipsis detection, and shared character budgets for LinkedIn/Substack copy surfacessrc/core/publish/research-page.ts—generateResearchPage(reads thesis/findings/bibliography from research doc + benchmark summary from results.json; description from draft frontmatter; skips analysis-opinion without artifacts)src/core/publish/platform-images.ts—ensurePlatformImages, platform specs, safe asset resolver, local Sharp transforms, deterministic fallback SVG, receipt/frontmatter updatessrc/core/publish/site.ts—createSitePR(dirty-state guardrail with rename/copy-aware porcelain parsing + path-scoped staging:content/posts/{slug}+ optionalcontent/research/{slug}, nogit add .),checkPreviewGate,resolveSiteRepoPath, git/gh subprocess wrappers (execFileSync only)src/core/publish/devto.ts—crosspostToDevTo,probeDevToForCanonical(paginated GET + canonical match + trailing-slash normalization),mapDevToTagssrc/core/publish/medium.ts—generateMediumPaste(H1 title + canonical footer)src/core/publish/substack.ts—generateSubstackPaste(H1 title + H2 subtitle layout)src/core/publish/repo.ts—pushCompanionRepo(gh repo view probe → gh repo create --source=. --push, with "already exists" race fallback),parseGitHubRemoteUrlhelper (SSH/HTTPS normalization),assertOriginMatches(trust-boundary guardrail: throws when local origin does not match expected{author.github}/{slug})src/core/publish/frontmatter.ts—updateFrontmatter(direct push to main,chore(post):prefix, checkout-before-mutation ordering,assertIndexCleanprecheck against operator-staged files,inspectAheadCommitshelper with strict subject + file match for crash-replay push-ahead)src/core/publish/readme.ts—updateProjectReadme(direct push to main,chore:prefix,config.projects[id]resolved against dirname(configPath), checkout-before-mutation ordering, sharedassertIndexCleanguardrail, strict crash-replay push-ahead via sharedinspectAheadCommits)src/core/publish/social.ts—generateSocialText,generateLinkedIn,generateHackerNews,containsEmoji(throws on emojis in generated output)src/core/publish/pipeline-types.ts—PipelineContext,StepOutcome,StepResult,StepDefinitionsrc/core/publish/pipeline-registry.ts—PIPELINE_STEPSarray (ordered 11 step-to-module bindings, translates module results toStepResult)src/core/publish/pipeline-runner.ts—runPipeline(acquires lock, reclaims stalerunning, reconciles pending rows against current config, executes steps in order, per-step transactional URL persistence, callscompletePublishUnderLockwithout lock re-entry)templates/research-page/template.mdx— research page scaffold with{{thesis}},{{findings}},{{bibliography}},{{methodology_summary}},{{open_questions}},{{repo_link}}placeholderstemplates/social/linkedin.md— LinkedIn template with{{hook}},{{body}},{{canonical_url}},{{hashtags}},{{timing}}placeholderstemplates/social/hackernews.md— HN template with{{title}},{{canonical_url}},{{first_comment}},{{repo_url}},{{timing}}placeholdersskills/blog-publish.md— publish workflow narration, 11-step descriptions, manual preview-gate at step 4, resume semantics, troubleshooting (missing DEVTO_API_KEY, gh auth, PR not merged)
Phase 7 — Lifecycle source files:
src/core/db/schema.ts— SCHEMA_VERSION=3, SCHEMA_V3_SQL (pipeline_steps canonical table-rebuild adding cycle_id + UNIQUE(post_slug, cycle_id, step_name)),update_cycles+unpublish_stepstables, partial-unique-indexidx_update_cycles_opensrc/core/db/database.ts— v3 migration block with FK-off toggle around transactional rebuildsrc/core/db/types.ts— UpdateCycleRow, UnpublishStepRow, UpdateCycleEndedReason typessrc/core/publish/types.ts— UPDATE_STEP_NAMES tuple, PublishMode union,stepNamesForMode()helper, cycle_id on PipelineStepRowsrc/core/publish/pipeline-types.ts— PipelineContext.publishMode + .cycleId + .lockTimeoutMs (test-only)src/core/publish/pipeline-runner.ts— mode-aware finalization (completePublishUnderLock vs completeUpdateUnderLock); passes ctx.lockTimeoutMs through to acquirePublishLocksrc/core/publish/pipeline-registry.ts— mode-aware verify step (checks evaluation_passed for initial, is_update_review + cycle timing for update); site-update step entry; updateDevToArticle dispatch for update modesrc/core/publish/phase.ts—finalizePipelineUnderLockshared PID-ownership helper,completeUpdateUnderLock(closes update_cycles row, increments update_count, writes update_completed metric, phase stays published);completeUnpublishUnderLockwired in via state.tssrc/core/publish/steps-crud.ts— every CRUD function gained trailing optionalcycleId: number = 0andpublishMode: PublishMode = 'initial'parameters; createPipelineSteps seeds step_number from tuple position per modesrc/core/publish/site.ts— createSitePR calls requireOriginMatch(expectedSiteCoords(config)); accepts SitePROverrides; checkPreviewGate also guards originsrc/core/publish/site-update.ts—createSiteUpdatewraps createSitePR with update-cycle-scoped branch/commit/PR strings derived fromupdate_cyclesrowsrc/core/publish/devto.ts—updateDevToArticleprobe-then-PUT with POST fallthrough on probe misssrc/core/publish/origin-guard.ts— SHARED trust-boundary module:parseGitHubRemoteUrl,getOriginState(tolerant),requireOriginMatch(strict),expectedSiteCoords(config-driven coords resolver),coordsMatchcase-insensitive helper,readOriginUrlwith LC_ALL=C locale pin and narrowed stderr catchsrc/core/config/types.ts— SiteConfig.github_repo optional, SiteUpdateMode, UnpublishConfig, extended UpdatesConfig (devto_update, refresh_paste_files, notice_template, require_summary, site_update_mode)src/core/config/loader.ts— DEFAULT_UNPUBLISH, extended DEFAULT_UPDATES, github_repo shape validation (owner/name)src/core/update/cycles.ts—openUpdateCycle/getOpenUpdateCycle/closeUpdateCycle/listUpdateCycles/requirePublishedPost; metrics writes inside transactions ('update_opened', 'update_aborted', 'update_completed')src/core/update/notice.ts—appendUpdateNoticewith cycle-keyed HTML comment regex, atomic temp+rename writesrc/cli/update.ts—runUpdateStart/Benchmark/Draft/Evaluate/Publish/Abort/Show+registerUpdate(program); slug validation + phase boundary + process.exitCode=1 pattern; passes{ isUpdateReview: true }to initEvaluationsrc/core/unpublish/state.ts—initUnpublishwith already-unpublished idempotency,completeUnpublishUnderLockvia shared finalizePipelineUnderLock; writes 'unpublish_started' + 'unpublished' metricssrc/core/unpublish/steps-crud.ts— parallel to publish/steps-crud but no cycle_id (unpublish has only one pass per post)src/core/unpublish/steps-registry.ts— UNPUBLISH_STEP_NAMES 7-tuplesrc/core/unpublish/runner.ts— mirrors publish runner; uses acquirePublishLock for mutual exclusion; accepts optional lockTimeoutMs (test-only injection seam)src/core/unpublish/devto.ts— PUT{ article: { published: false } }per spike, probe-then-mutate, probe-miss is skip not errorsrc/core/unpublish/medium.ts/src/core/unpublish/substack.ts— manual-removal instruction markdown generators (no API)src/core/unpublish/site.ts—createSiteRevertPR(PR-only, no direct-push grep-verified);flipPublishedToFalse;checkUnpublishPreviewGate; calls requireOriginMatch(expectedSiteCoords(config)) before any pushsrc/core/unpublish/readme.ts—revertProjectReadmeLinkwith THREE explicit skip paths; assertIndexClean before git add + requireOriginMatch before git push origin main (Phase 6 symmetry documented in lifecycle.md)src/cli/unpublish.ts—runUnpublishStart --confirm / runUnpublishShow, slug validation, --confirm required before any DB/network worksrc/cli/publish.ts— publish guard: refuses on open update cycle, actionable error messagesrc/core/draft/frontmatter.ts— PostFrontmatter interface extended withunpublished_at,updated_at,update_count,substack_url; serializer + parser round-trip, with update_count normalized to number even when YAML emits stringsrc/core/evaluate/state.ts—initEvaluation({ isUpdateReview: true })option sets manifestis_update_cycle=true; recordReview reads the flag and setsis_update_review=1on every inserted row (explicit, not inferred from cycles.length)fixtures/frontmatter-phase7/*.mdx— 6 canonical Phase 7 frontmatter shapes (legacy, initial-published, updated-once, updated-twice, unpublished, updated-then-unpublished) + README documenting the cross-repo contract between m0lz.01 and m0lz.00skills/blog-pipeline.md/skills/blog-update.md/skills/blog-unpublish.md— orchestrator skills with exact H2 set {Preflight, Workflow, CLI Reference, Troubleshooting, Degraded Mode}; cross-checked mechanically against src/cli/index.ts registrations.codex/rules/lifecycle.md— path-scoped rule doc: publishMode dispatch, first-class update_cycles rows, partial-unique-index constraint, explicit isUpdateReview flag, cycle-keyed notice marker, publish guard, shared per-slug lock, shared finalize helper, metrics audit log, unpublish trust boundaries, origin-guard tolerant/strict APIs, readme-revert direct-push policy clarificationdocs/spikes/forem-put-semantics.md— canonical source for Forem PUT body shapes (unpublish + update), referenced by devto.ts implementations
v0.3 Dogfood Hardening source files:
src/core/draft/content-types.ts—extractProjectIdFromPrompthelper (generic catalog-style/\b([a-z][a-z0-9]*\.\d+)\b/iregex, lowercased first match)src/core/research/state.ts—initResearchPostaccepts trailing optionalprojectIdparameter, inserts intoposts.project_id, throws[AGENT_ERROR] PROJECT_UNLINKEDwhen contentType is project-launch and projectId is null/emptysrc/cli/research.ts—runResearchInitresolves projectId via--projectflag → prompt-regex fallback → null, threads intoinitResearchPost;--project <id>Commander option registeredsrc/core/draft/frontmatter.ts—generateFrontmatter(post, config, configPath)resolvescompanion_repoviaresolveCompanionRepoFromProject(reads git origin ofconfig.projects[post.project_id], normalizes to canonical HTTPS, best-effort on failure); falls back tohas_benchmarksheuristic only when project resolution is nullsrc/core/draft/state.ts—initDraftthreadsconfigPaththrough togenerateFrontmatter;regenerateDraftFrontmatterexported (rewrites frontmatter block in place preserving body byte-for-byte, rejects phase=published, writes.frontmatter-regenerated.jsonaudit receipt)src/core/publish/origin-guard.ts—readOriginUrl+parseGitHubRemoteUrlexported for reuse;assertOriginInSync(repoPath, branch)fetches + runsrev-list --count origin/<branch>..<branch>and throws[AGENT_ERROR] ORIGIN_OUT_OF_SYNCon non-zero ahead count or "couldn't find remote ref" fetch errorsrc/core/publish/site.ts—SitePROverrides.allowMainAheadoptional flag;createSitePRcallsassertOriginInSyncbefore mutations unless override set, emits[WARN] site-pr: --allow-main-ahead bypassed origin sync checkon bypass;checkPreviewGatetakesdbparameter and returnsPreviewGateResult extends PreviewUrlssrc/core/publish/preview-urls.ts— new module:PreviewUrlstype +computePreviewUrls(post, config, configPath, researchPagesDir)helper (canonicalUrl always emitted, supplementaryUrl from research-pages file existence, companionRepoUrl prefers post.repo_url then config.projects origin lookup)src/core/publish/pipeline-registry.ts— site-pr step passesallowMainAheadfrom ctx; preview-gate step includes preview URLs indatasrc/core/publish/pipeline-types.ts—PipelineContext.allowMainAheadoptional fieldsrc/cli/publish.ts—runPublishStartremoves phase-conditional skip ofinitPublish(now called for both evaluate and publish phases); logsseeded N pipeline_steps for <slug>when insertion occurs; rejects phase=published with actionable error;--allow-main-aheadCommander option registered;tryLoadPreviewUrlshelper surfacesdata.preview_urlsin thePublishPipelineJSON envelopesrc/cli/draft.ts—runDraftRegenerateFrontmatterCLI handler + Commanderregenerate-frontmatter <slug>registrationsrc/core/plan-file/schema.ts—'blog draft regenerate-frontmatter'added toSLUG_BEARING_STEP_COMMANDS(inheritsKNOWN_LEAF_COMMANDSvia spread).claude-plugin/skills/blog/JOURNEYS.md— Journey E added (frontmatter-recovery pattern).claude-plugin/skills/blog/REFERENCES.md— Draft-frontmatter recovery section documentingregenerate-frontmattersemantics
Release Prep source files:
src/core/paths.ts— shared package-root resolver:findPackageRoot(moduleUrl)walks up from a module URL until it finds apackage.json,PACKAGE_ROOT= the package directory computed eagerly at module load viaimport.meta.url,TEMPLATES_ROOT=resolve(PACKAGE_ROOT, 'templates'). Every template-reading code path in the repo routes through one of these two constants — no inlinefileURLToPath/ offset-arithmetic incantations remainsrc/cli/init.ts— hard-fail on missing shipped example files (.blogrc.example.yaml/.env.example) with diagnostic pointing at a packaging bug;packageRootis an injectable third parameter (defaults toPACKAGE_ROOT) so regression tests can exercise the hard-fail path via a tmpdir without monkey-patching the modulesrc/cli/publish.ts/src/cli/update.ts— useTEMPLATES_ROOTfor package-shipped template reads; all.blog-agent/...and.blogrc.yamlpaths remain CWD-relative (operator state, not package state — Negative contract criterion #11 locks this distinction in)src/core/benchmark/companion.ts/src/core/research/document.ts— same refactor symmetrically applied:resolve(TEMPLATES_ROOT, '<subpath>')instead of inlinefileURLToPathscripts/verify-pack.mjs— four-layer packaging gate wired into bothnpm run verify-packandprepublishOnly: (1) ALLOWED_PATTERNS whitelist, (2) FORBIDDEN_PATTERNS denylist (no secrets,.js.map,state.db,.blog-agent/,src/,tests/,.codex/), (3) REQUIRED_FILES manifest of 12 runtime-critical paths (catches silent deletions the allowlist alone would miss — Release Prep Pass 1 Codex Finding #3), (4) src→dist compiled-closure check walkingsrc/**/*.tsand asserting every correspondingdist/**/*.jsis in the tarball (Release Prep Pass 2 Codex Finding #1 — the allowlist glob + single-entrypoint REQUIRED_FILES would silently pass a missing compiled module without this)package.json—build: "clean-dist && tsc && chmod-bin"(prevents stale compiled artifacts from shipping AND restores owner-execute bit on the CLI entrypoint that tsc strips on every rebuild — seescripts/chmod-bin.mjs);prepublishOnly: "lint && build && test && verify-pack"(publish-time gate that cannot be bypassed);filesuses globs (dist/**/*.js,dist/**/*.d.ts— excludes.js.mapwithout a second build config); publish surface fields (repository,homepage,bugs,keywords,author);engines.node: ">=20.3.0"(pinned forsharp+ recursivereaddirSync)scripts/chmod-bin.mjs— post-tsc step thatchmodSync(dist/cli/index.js, 0o755). Covers the failure mode wheretscregeneratesdist/cli/index.jsas 0644 and a pre-existingnpm linksymlink (/opt/homebrew/bin/blog → dist/cli/index.js) dereferences to a non-executable target. Grep-verifiable: the build script's terminal step imports this file;tests/build-bin-executable.test.tsasserts the final mode bit..claude/rules/voice.md— dev-facing voice rule (path-scoped auto-load for draft/evaluate/skill files). Hard rules: no hedges, no tricolon stacks, no topic-restatement transitions, no smarmy openers, no undefined jargon, ~1 em dash per 500 words max with period/comma/parens/cut substitution test, no emojis. Two accessibility tests (newcomer + expert). Structural reviewer loads these hard rules and emitscategory='voice'issues when the draft drifts. Must stay byte-parity with.claude-plugin/skills/blog/VOICE.md(enforced bytests/voice-parity.test.ts)..claude-plugin/skills/blog/VOICE.md— plugin-shipped voice rule (no frontmatter; plugin skills don't path-scope). Loaded by/blogSKILL.md when composingblog research set-sectionprose so hash-bound section content is voice-compliant before entering the hash gate. Byte-identical to.claude/rules/voice.mdbody (parity test blocks drift).src/core/draft/template.ts—renderDraftTemplateprepends aVOICE_COMMENTJSX block between frontmatter and the first##section; MDX strips JSX comments at compile time so the comment survives as context for the draft author/agent but is invisible in the rendered hub output..github/workflows/ci.yml— single Node 20 job, step order: checkout → setup-node → npm ci → lint → build → test → verify-pack (build BEFORE test — Release Prep Pass 1 Codex Finding #1; ensures the CWD-independence integration test always runs against fresh dist)CHANGELOG.md— Keep-a-Changelog format, single[0.1.0]section covering Phases 1–7 plus the template-path fixRELEASING.md— literal v0.1.0 sequence + subsequent-releases template; both sections havemain-branch + clean-tree fail-fast preflight guards; pre-publishgit push --atomic --dry-runcheck; atomicgit push --atomic origin main refs/tags/vX.Y.Z(not--follow-tags) so branch + tag move together;gh release create --verify-tagprevents silent wrong-commit tag creation; Recovery section structured around registry-state check (npm view m0lz-01@X.Y.Z version) with three cases: A=live/push-only-with-rebase-and-re-tag, B=E404/safe-retry, C=abandon (uses--mixedreset withgit status/git logpreflights); subsequent-release flow has its own two-commit recovery block withHEAD~2(notHEAD~1).github/PULL_REQUEST_TEMPLATE.md— ≤10 line checklist with absolute GitHub URLs to AGENTS.md (relative paths 404 in GitHub's PR body rendering).nvmrc—20.3.0(contributor convenience; matches the packageengines.nodefloor required bysharp)README.md— CI badge at top;## Installleads withnpx m0lz-01 init --import(dev install moved to new## Developmentsection);## Changelog+## Project Statusone-liners
/blog Skill Plugin source files:
src/core/workspace/root.ts—findWorkspaceRoot(cwd, {override, envVar})ancestor-walks to.blog-agent/state.db; throwsWorkspaceNotFoundErrorwith actionable hint citing--workspace+BLOG_WORKSPACEsrc/core/workspace/user-path.ts—resolveUserPath(p)uses_BLOG_ORIGINAL_CWDenv (set by startup shim) to resolve relative path flags against the operator's original cwd, not the chdir'd workspace rootsrc/core/workspace/resolve.ts—workspaceRelative(subpath)helper for explicit workspace-relative path construction (optional; module-levelresolve(...)constants work after chdir)src/cli/index.ts— startup shim: parse--workspaceearly,findWorkspaceRoot+process.chdir()BEFORE dynamic-importing command modules (so module-levelresolve('.blog-agent/...')constants evaluate against post-chdir cwd); whitelistsinit,help,agent,--help,--versionto skip the check; registersagentalongside the other commandssrc/core/json-envelope.ts—JsonEnvelope<K, D>type +makeEnvelope(kind, data)+printEnvelope(kind, data)writing to stdout with 2-space indentsrc/core/plan-file/schema.ts—PlanFiletype (schema v2),PLAN_CONTENT_TYPES,PLAN_DEPTHS,KNOWN_SUBCOMMANDS,DENY_STEP_SUBCOMMANDS(bannedagentnesting),BANNED_ARG_FLAGS(--workspace/--help/-h/--version/-V +=forms; the CLI startup shim hoists --workspace from any argv position — plan steps must not carry it),SLUG_BEARING_STEP_COMMANDS(everyblog X Y <slug>subcommand — validator requires args[0] === plan.slug),KNOWN_LEAF_COMMANDS(exhaustive allowlist of TRUE leaves: blog status / blog metrics + SLUG_BEARING; namespace parents likeblog ideas/blog researchexcluded to prevent args-smuggling via Commander's default-action pattern)src/core/plan-file/hash.ts—canonicalPlanJSON(plan)dropsapproved_at+payload_hashand stable-stringifies (recursive key sort, arrays preserve order);computePlanHash(plan)=sha256(canonicalPlanJSON(plan))src/core/plan-file/validator.ts—validatePlanSchema(raw)validates required fields + enums + step shape +validateSlug(plan.slug)at schema layer (hand-authored traversal slugs rejected, not just CLI-generated ones); per-step: command starts with "blog ", first word in KNOWN_SUBCOMMANDS, no flag-like tokens in command, canonical single-space form (no whitespace variants), exact match in KNOWN_LEAF_COMMANDS, banned flags in args,args[0] === plan.slugfor slug-bearing commands;validatePlanForApply(raw, {workspaceRoot})adds NO_APPROVAL / HASH_MISMATCH / WORKSPACE_MISMATCH;readPlanFile(path)parses JSON with SCHEMA_INVALID on parse failuresrc/core/plan-file/apply.ts—applyPlan(plan, opts)holds slug-scoped lock, DB-authoritative step state (schema v4 agent_plan_runs + agent_plan_steps), pre-spawn sentinel for crash recovery, pins spawned child to plan.workspace_root (prepend--workspace+ scrub BLOG_WORKSPACE from child env), captures exit/stdout_tail/stderr_tail/duration_ms per step, writes receipt at.blog-agent/plans/<slug>.receipt.jsonatomically viarenameSync(audit mirror, NOT trust input), resumes via completed-step skip derived from DB,--restartclears all open runs for slug + sentinels, throws ApplyError with UNKNOWN_COMMAND/STEP_FAILED/RECEIPT_CONFLICT/RECEIPT_HASH_MISMATCH/CRASH_RECOVERY_REQUIREDsrc/core/plan-file/apply-lock.ts— slug-scoped O_CREAT|O_EXCL lockfile with JSON stamp (pid + acquiredAt); honest PID-liveness policy (no false PID-reuse hardening claim); tolerant of legacy bare-PID format; reclaims ESRCH-dead holders + corrupt/empty lockfilessrc/core/db/schema.ts—SCHEMA_VERSION=4,SCHEMA_V4_SQLaddsagent_plan_runs+agent_plan_stepstables with FK CASCADE; receipt JSON is demoted to audit mirrorsrc/core/db/database.ts— v4 migration block inmigrate()transactionsrc/cli/agent.ts—preflight(trusts post-chdir process.cwd(), omits envVar fallthrough so --workspace > BLOG_WORKSPACE precedence holds),plan(writes skeleton withapproved_at: null, validateSlug at CLI boundary, clampOutputPath enforces<workspace>/.blog-agent/plans/*.plan.json with realpath-aware parent + lstat-based leaf symlink rejection),approve(atomically sets approved_at + payload_hash),verify(dry-run validation),apply(validate + execute + write receipt); error taxonomy emitted as[AGENT_ERROR] <CODE>: <msg>on stderr with exit 2 (validation) or 1 (STEP_FAILED)src/cli/index.ts— startup shim: parses both--workspace <path>and--workspace=<path>compact form, rejects empty operands explicitly, excludes workspace-operand slots from positional walk sofirstPositionalcorrectly identifies subcommand; WORKSPACE_FREE_COMMANDS narrowed toinit,help; AGENT_WORKSPACE_FREE_SUBCOMMANDS=preflight(plan/approve/verify/apply require real workspace)src/cli/status.ts,src/cli/publish.ts,src/cli/update.ts,src/cli/unpublish.ts,src/cli/evaluate.ts—--jsonflag on show commands; branches before human-table render, prints envelope + returns.claude-plugin/plugin.json— manifest declaring theblogskill.claude-plugin/skills/blog/SKILL.md— ≤200-line orchestration prose: preflight viablog agent preflight --json, intent classification, slug collision, plan proposal, approval gate invokingapprove → verify → apply, error taxonomy.claude-plugin/skills/blog/REFERENCES.md— CLI surface + phase state machine + plan schema + receipt shape + legacyskills/blog-*.mdpointers.claude-plugin/skills/blog/JOURNEYS.md— four worked journeys: new project launch, technical deep-dive with benchmark, update, unpublish.claude-plugin/skills/blog/CHECKPOINTS.md— every human-in-the-loop moment, pre-apply (skill-owned) and post-apply (CLI-owned).claude/skills/blog— relative symlink →../../.claude-plugin/skills/blogfor contributor sessions.claude/rules/skills.md— path-scoped rules for.claude-plugin/skills/**(destructive-only-via-apply, --json discipline, no hardcoded identity)scripts/verify-pack.mjs— allowlist.claude-plugin/**; REQUIRED_FILES adds.claude-plugin/plugin.json+.claude-plugin/skills/blog/SKILL.mdpackage.json—filesarray adds.claude-plugin/**docs/plugin-install.md— three install paths (npm-bundled, repo clone, contributor) + troubleshooting
Expected results
All tests should pass. If any fail after your changes:
- Check if you modified the source files listed above
- Read the failing test to understand what behavior it expects
- Fix your code to preserve the expected behavior, or update the test if the behavior change is intentional
Updating the regression suite
After running the regression suite and before finishing the review, check if any test files touched in this session are NOT already in the suite above:
# Compare test files modified in uncommitted changes against the suite list
git diff --name-only HEAD -- 'tests/*.test.ts'
git status --short -- 'tests/*.test.ts'For each test file that exercises a newly shipped or migrated feature and is NOT already in the regression suite:
- Add it to the
npx vitest runcommand in the bash block above - Add a row to the "What each test covers" table with: file name, test count, feature name, what it validates
- Add any new source files to the "Source files these tests protect" list
- Add a line to the Phase 4 output format checklist under the current milestone
- Start a new milestone section (e.g.,
**Phase 2 — Research (feature/phase-2-research)**) when the next phase ships
This ensures the suite is always exhaustive: every feature we ship gets regression-protected automatically.
Phase 2: Full Validation
After regression tests pass, run the full suite:
npx tsc --noEmit
npm test
npm run buildExpected baseline: 0 TypeScript errors, 1137 tests passing across 79 suites, clean build. v0.3 dogfood-hardening added +43 tests across +5 new suites — research-project-linking (15), publish-origin-divergence (6), publish-seed-on-start (4), preview-gate-urls (9), draft-regenerate-frontmatter (9) — plus +8 tests in draft-frontmatter.test.ts for companion_repo resolution. Platform article images added Dev.to/Medium/Substack-preview coverage and expanded draft/crosspost coverage. Durable distribution kits add distribution-kit.test.ts, publish-distribution-kit.test.ts, publish-table-assets.test.ts, and CLI/config/plan-file coverage. Benchmark phase-gates, publish artifact verification, review-inventory tripwires, natural Substack subtitle fitting/failure, pre-clipped subtitle rejection, public-only LinkedIn copy, no-ellipsis HN copy, sanitized image prompts, bounded local-card SVG text, local-card LinkedIn image defaults, copyable chart paste output, portable table image assets, complete-bundle backfill, disabled-channel cleanup, paste-step table verification, stale deletion crash-replay coverage, Medium/Substack upload-checklist handoff, site/direct-push cleanup staging, mirrored Claude review inventory coverage, readable row-card table images with snake_case preservation, pre-clipped LinkedIn fallback copy, legacy prompt-only manifest loading, symlink-protected local-card writes, expected-hash manifest reuse, social-text tamper regeneration, and self-heal source-plan selection bring the current suite to 1137/79; any drift from this baseline is a signal to investigate before merging.
Phase 3: Code Review of Current Changes
git branch --show-current
git statusIf on a feature branch, diff against main to see the full feature scope:
git diff main...HEAD
git diff main...HEAD --statIf on main, diff against the last commit:
git diff HEADIf reviewing a specific commit, check it out or diff against it.
Focus Areas
- Logic errors and incorrect behavior
- Edge cases that aren't handled
- Null/undefined reference issues
- Race conditions or concurrency issues — especially relevant when CLI and Codex skills both write to
state.db - Security vulnerabilities — SQL injection (check for string interpolation instead of
?placeholders), path traversal in import, env var leakage - Resource management — unclosed database connections (
closeDatabasecalled on all paths?), leaked file handles, temp dirs not cleaned in tests - API contract violations — PostFrontmatter schema drift vs m0lz.00, CLI flag changes that break scripts
- Caching bugs — the SQLite layer has no caching, but watch for prepared-statement reuse across closed DBs
- Pattern violations — check AGENTS.md and
.codex/rules/for project conventions:- ESM imports MUST use
.jsextension on internal imports - SQL queries MUST use parameterized statements (
?or@named) - better-sqlite3 is synchronous — no
async/awaitfor DB operations - CLI commands are non-interactive (Commander.js options only, no readline prompts)
- No emojis in content or user-facing output
- Pipeline operations must be idempotent
- ESM imports MUST use
Rules
- Use sub-agents to explore the codebase in parallel for efficiency
- Report pre-existing bugs found near the changed code — code quality matters everywhere
- Do NOT report speculative or low-confidence issues — conclusions must be based on actual code understanding
- If reviewing a specific git commit, note that local code may differ from that commit
Phase 4: Output Format
Migration Status
Schema Drift: NONE / DETECTED (tables/columns affected)
SCHEMA_VERSION Bumped: YES / NO / N/A
Fresh init works: PASS / FAIL
Action: Bump SCHEMA_VERSION and add migration block, or N/ARegression Suite Results
Regression Suite: PASS / FAIL
Phase 1 — Foundation:
- Database schema + connection (7 tests): ✓ / ✗
- Config loader (10 tests): ✓ / ✗
- Post import (5 tests): ✓ / ✗
- Ideas backlog CRUD (11 tests): ✓ / ✗
- Content type detection (6 tests): ✓ / ✗
- CLI handlers (13 tests): ✓ / ✗
Phase 2 — Research:
- Schema v1->v2 migration (3 tests): ✓ / ✗
- Research post lifecycle (9 tests): ✓ / ✗
- Source management (11 tests): ✓ / ✗
- Research documents (18 tests): ✓ / ✗
- Research CLI handlers (13 tests): ✓ / ✗
Phase 3 — Benchmark:
- Environment capture (4 tests): ✓ / ✗
- Benchmark state lifecycle (17 tests): ✓ / ✗
- Results storage (11 tests): ✓ / ✗
- Companion repo scaffolding (6 tests): ✓ / ✗
- Benchmark CLI handlers (19 tests): ✓ / ✗
- Benchmark repair CLI (7 tests): ✓ / ✗
Phase 4 — Draft:
- PostFrontmatter schema (32 tests): ✓ / ✗
- Draft state lifecycle (17 tests): ✓ / ✗
- Benchmark data formatting (9 tests): ✓ / ✗
- Tag taxonomy reader (6 tests): ✓ / ✗
- Draft CLI handlers (33 tests): ✓ / ✗
Phase 5 — Evaluate:
- ReviewerOutput schema (21 tests): ✓ / ✗
- Structural autocheck lints (15 tests): ✓ / ✗
- Synthesis + matching (24 tests): ✓ / ✗
- Report renderer (4 tests): ✓ / ✗
- Evaluation state lifecycle (81 tests): ✓ / ✗
- Evaluate CLI handlers (24 tests): ✓ / ✗
Phase 6 — Publish:
- Phase + steps-crud + lock + crash-safety + reconcile (43 tests): ✓ / ✗
- Pipeline runner + crash-safety + reconcile regressions (20 tests): ✓ / ✗
- MDX → Markdown converter (15 tests): ✓ / ✗
- Portable table asset rendering (11 tests): ✓ / ✗
- Site PR + preview gate + dirty-state + rename/copy guardrail (21 tests): ✓ / ✗
- Dev.to probe-then-create + Medium + Substack paste (40 tests): ✓ / ✗
- Deterministic platform image generation (22 tests): ✓ / ✗
- Social text: LinkedIn + Hacker News (17 tests): ✓ / ✗
- Durable distribution-kit generation (28 tests): ✓ / ✗
- Pre-site generation + strict distribution persistence (26 tests): ✓ / ✗
- Research page generator (15 tests): ✓ / ✗
- Companion repo probe-then-create + origin-URL guardrail (21 tests): ✓ / ✗
- Publish CLI handlers + ctx.urls hydration + distribution-kit backfill (20 tests): ✓ / ✗
- updateFrontmatter + updateProjectReadme + index-clean + crash-replay (20 tests): ✓ / ✗
Phase 7 — Lifecycle:
- Schema v3 canonical table-rebuild + partial-unique-index (12 tests): ✓ / ✗
- Update-cycle lifecycle (17 tests): ✓ / ✗
- Cycle-keyed update-notice marker (13 tests): ✓ / ✗
- Update CLI handlers (abort artifact preservation) (7 tests): ✓ / ✗
- Update Dev.to probe→PUT / miss→POST (4 tests): ✓ / ✗
- Update publish unit coverage: site-update + completeUpdateUnderLock (5 tests): ✓ / ✗
- Publish guard: refuses on open update cycle (2 tests): ✓ / ✗
- Unpublish state lifecycle (6 tests): ✓ / ✗
- Unpublish CLI + --confirm + fetch-spy (3 tests): ✓ / ✗
- Unpublish readme-revert trust boundaries (5 tests): ✓ / ✗
- Unpublish site revert PR-only (5 tests): ✓ / ✗
- Unpublish Dev.to PUT published:false (7 tests): ✓ / ✗
- Unpublish pipeline E2E (4 tests): ✓ / ✗
- Orchestrator skills cross-reference (2 tests): ✓ / ✗
- TRUE E2E update-mode runPipeline (3 tests): ✓ / ✗
- Cross-flow lock contention (same-slug + disjoint) (3 tests): ✓ / ✗
- PostFrontmatter Phase 7 round-trip + cross-repo fixtures (15 tests): ✓ / ✗
- Split origin-guard tolerant/strict APIs (16 tests): ✓ / ✗
- Pipeline registry ↔ step-tuple invariants (8 tests): ✓ / ✗
Release Prep:
- Shared package-root resolver (4 tests): ✓ / ✗
- CLI CWD-independence integration (1 test): ✓ / ✗
- init.ts hard-fail regression — absent shipped templates (2 tests in cli.test.ts): ✓ / ✗
/blog Skill Plugin:
- Workspace-root detection (11 tests): ✓ / ✗
- `--json` envelope surface (6 tests): ✓ / ✗
- Plan schema + hash + validator + apply + receipt + lock (64 tests): ✓ / ✗
- Plugin static shape + SKILL discipline + sibling-doc parity + install-docs (22 tests): ✓ / ✗
- Real skill-to-CLI handoff end-to-end + crash recovery + workspace pinning (25 tests): ✓ / ✗
v0.3 Dogfood Hardening:
- Research project linking: `--project` flag + prompt regex + PROJECT_UNLINKED guard (15 tests): ✓ / ✗
- Publish site-pr origin-sync divergence refusal + allow-main-ahead override (6 tests): ✓ / ✗
- runPublishStart always seeds pipeline_steps on phase=publish (4 tests): ✓ / ✗
- Preview-gate emits canonical/supplementary/companion URLs in JSON envelope (9 tests): ✓ / ✗
- blog draft regenerate-frontmatter recovery command (9 tests): ✓ / ✗
Full Suite: X passing, Y failing (baseline: 1137 passing across 79 suites)
Lint: {error count} errors (baseline: 0)
Build: PASS / FAILContract Evaluation (if applicable)
Contract: {feature name} — Tier {N}
Evaluator: Isolated sub-agent (no implementation context)
| Criterion | Threshold | Score | Pass |
|-----------|-----------|-------|------|
| {name} | {T}/10 | {S}/10 | YES/NO |
Overall: PASS / FAILIf no contract was found in the plan, output: Contract: N/A — no contract in plan
Code Review Findings
Group findings by severity:
Critical — Must fix before merge (bugs, security, data loss)
file:line— description of the issue and recommended fix
Warning — Should fix (performance, maintainability, pattern violations)
file:line— description and suggestion
Suggestion — Consider improving (readability, minor optimizations)
file:line— description and suggestion
Positive — What's done well (reinforce good patterns)
- Description of what was done right
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.

