← back to Cli Printing Press
feat(cli): flow transcendence features into generated READMEs with integrity validation (#137)
96b9b42cfd31e579918b77737eb4c0ef0565eaad · 2026-04-05 18:35:47 -0700 · Trevin Chow
Transcendence features (novel capabilities invented during absorb, not found
in any existing tool) are the printed CLI's key differentiator but were never
surfaced in the README. This change adds end-to-end support:
Pipeline: research.json carries novel_features (planned) and
novel_features_built (verified). The skill writes planned features at Step
1.5e; dogfood validates which survived the build and writes the verified
subset. The README template renders a "What's New Here" section from verified
features only.
Integrity: dogfood --research-dir cross-references planned features against
registered cobra commands. Missing features produce a WARN with specific
command names. The zero-survivor case correctly produces an empty verified
list (not a fallback to the aspirational list). fullrun.go passes the
research dir so validation runs in the standard MakeBestCLI path.
Retrofit: polish-worker checks manuscripts for research.json and injects the
section into already-printed CLIs whose README lacks it.
Tested against the Cal.com CLI: 1/5 planned transcendence features survived
(noshow), correctly detected by the new dogfood check.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M agents/polish-worker.mdA docs/plans/2026-04-05-002-feat-transcendence-readme-flow-plan.mdM internal/cli/dogfood.goM internal/cli/root.goM internal/generator/generator.goM internal/generator/generator_test.goM internal/generator/templates/readme.md.tmplM internal/pipeline/dogfood.goM internal/pipeline/dogfood_test.goM internal/pipeline/fullrun.goM internal/pipeline/research.goM internal/pipeline/research_test.goM skills/printing-press/SKILL.md
Diff
commit 96b9b42cfd31e579918b77737eb4c0ef0565eaad
Author: Trevin Chow <trevin@trevinchow.com>
Date: Sun Apr 5 18:35:47 2026 -0700
feat(cli): flow transcendence features into generated READMEs with integrity validation (#137)
Transcendence features (novel capabilities invented during absorb, not found
in any existing tool) are the printed CLI's key differentiator but were never
surfaced in the README. This change adds end-to-end support:
Pipeline: research.json carries novel_features (planned) and
novel_features_built (verified). The skill writes planned features at Step
1.5e; dogfood validates which survived the build and writes the verified
subset. The README template renders a "What's New Here" section from verified
features only.
Integrity: dogfood --research-dir cross-references planned features against
registered cobra commands. Missing features produce a WARN with specific
command names. The zero-survivor case correctly produces an empty verified
list (not a fallback to the aspirational list). fullrun.go passes the
research dir so validation runs in the standard MakeBestCLI path.
Retrofit: polish-worker checks manuscripts for research.json and injects the
section into already-printed CLIs whose README lacks it.
Tested against the Cal.com CLI: 1/5 planned transcendence features survived
(noshow), correctly detected by the new dogfood check.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
agents/polish-worker.md | 39 ++++
...4-05-002-feat-transcendence-readme-flow-plan.md | 224 +++++++++++++++++++++
internal/cli/dogfood.go | 25 ++-
internal/cli/root.go | 25 ++-
internal/generator/generator.go | 11 +
internal/generator/generator_test.go | 79 ++++++++
internal/generator/templates/readme.md.tmpl | 13 ++
internal/pipeline/dogfood.go | 128 ++++++++++--
internal/pipeline/dogfood_test.go | 150 ++++++++++++++
internal/pipeline/fullrun.go | 2 +-
internal/pipeline/research.go | 27 +++
internal/pipeline/research_test.go | 78 +++++++
skills/printing-press/SKILL.md | 27 ++-
13 files changed, 808 insertions(+), 20 deletions(-)
diff --git a/agents/polish-worker.md b/agents/polish-worker.md
index d91b8820..53d07f1b 100644
--- a/agents/polish-worker.md
+++ b/agents/polish-worker.md
@@ -117,6 +117,45 @@ users will get errors on their first try.
Replace `<!-- DOCTOR_OUTPUT -->` with real `doctor` output. Empty
placeholders signal an unfinished product.
+#### Inject novel features from research
+
+If the README lacks a `## What's New Here` section, check whether the
+manuscript archive has novel features to surface:
+
+```bash
+PRESS_HOME="$HOME/printing-press"
+API_SLUG="${CLI_NAME%-pp-cli}"
+RESEARCH_JSON=""
+for f in "$PRESS_HOME/manuscripts/$CLI_NAME"/*/research.json \
+ "$PRESS_HOME/manuscripts/$API_SLUG"/*/research.json; do
+ if [ -f "$f" ]; then RESEARCH_JSON="$f"; break; fi
+done
+```
+
+If `RESEARCH_JSON` exists, read it and check for a `novel_features` array.
+If that array is non-empty and the README has no `## What's New Here`
+heading, inject the section **before `## Sources & Inspiration`** (or
+before the `Generated by` footer if no Sources section exists).
+
+Format each feature exactly as the generator template does:
+
+```markdown
+## What's New Here
+
+These capabilities don't exist in any other tool for this API.
+
+### `<CLI_NAME> <command>`
+
+<description>
+
+> <rationale>
+```
+
+Before injecting, verify each feature's `command` actually exists in the
+built CLI by running `./$CLI_NAME <command> --help 2>/dev/null`. Skip any
+feature whose command does not exist — it may have been renamed or dropped
+during generation.
+
#### Required sections (must be present and correct)
1. **Title**: "# <Product Name> CLI" — use the product's real name with
diff --git a/docs/plans/2026-04-05-002-feat-transcendence-readme-flow-plan.md b/docs/plans/2026-04-05-002-feat-transcendence-readme-flow-plan.md
new file mode 100644
index 00000000..9097d1ec
--- /dev/null
+++ b/docs/plans/2026-04-05-002-feat-transcendence-readme-flow-plan.md
@@ -0,0 +1,224 @@
+---
+title: "feat: Flow transcendence features into generated READMEs"
+type: feat
+status: completed
+date: 2026-04-05
+---
+
+# feat: Flow transcendence features into generated READMEs
+
+## Overview
+
+Transcendence features (novel capabilities the machine invents during absorb, not found in any existing tool) are the printed CLI's key differentiator. They get identified, scored, and built into the CLI -- but never called out in the README. This change extends the existing `research.json` sidecar to carry novel features through the generator pipeline and renders them in a dedicated README section.
+
+## Problem Frame
+
+During the absorb phase (Phase 1.5), the skill identifies compound use cases only possible with our local data layer. These score >= 5/10 on domain fit, user pain, build feasibility, and research backing. The skill builds them as Priority 2 commands. But the data stays in the absorb manifest (markdown) and never reaches the README because:
+
+1. `research.json` has no field for individual novel features
+2. `Generator` struct doesn't carry them
+3. `readme.md.tmpl` has no section for them
+
+The Sources & Inspiration section credits tools we learned from. There's no counterpart section saying "here's what we invented."
+
+## Requirements Trace
+
+- R1. Generated READMEs must include a section highlighting transcendence features when they exist
+- R2. The section must show the feature name, CLI command, user-facing description, and rationale
+- R3. The section must be absent (no empty heading, no placeholder) when no novel features qualify
+- R4. The data must flow through the existing `research.json` -> `loadResearchSources()` -> Generator -> template pipeline
+- R5. Existing scored README sections (Quick Start, Agent Usage, Health Check, Troubleshooting, Cookbook) must remain intact
+
+## Scope Boundaries
+
+- Does not change the absorb manifest format (it already has the transcendence table)
+- Does not change the absorb scoring framework
+- Does not add a new CLI subcommand
+- Does not change the `--research-dir` flag behavior
+- Does not affect the automated `RunResearch()` code path (it doesn't produce novel features)
+
+## Context & Research
+
+### Relevant Code and Patterns
+
+- `internal/pipeline/research.go:23-32` -- `ResearchResult` struct, the JSON contract between skill and generator
+- `internal/pipeline/research.go:173-178` -- `ReadmeSource` type (pipeline side)
+- `internal/pipeline/research.go:214-234` -- `SourcesForREADME()` filter and sort
+- `internal/generator/generator.go:29-34` -- `ReadmeSource` type (generator side, mirrors pipeline type)
+- `internal/generator/generator.go:36-46` -- `Generator` struct with `Sources` and `DiscoveryPages`
+- `internal/generator/generator.go:237-256` -- `readmeTemplateData` and `readmeData()` method
+- `internal/cli/root.go:676-696` -- `loadResearchSources()` bridges pipeline -> generator
+- `internal/generator/templates/readme.md.tmpl:210-227` -- "Sources & Inspiration" conditional section
+- `skills/printing-press/SKILL.md:815-843` -- Step 1.5e writes research.json
+
+### Institutional Learnings
+
+- `docs/plans/2026-04-01-008-feat-readme-source-credits-plan.md` -- Established the exact pattern this plan follows: skill writes research.json -> `loadResearchSources()` reads it -> Generator populates template. Canonical reference for sidecar data flow.
+- `docs/retros/2026-04-01-steam-run4-retro.md` -- README template must always emit the 5 scored sections. New sections must not displace them.
+- `docs/plans/2026-03-30-002-feat-readme-source-credits-plan.md` -- Use template-based rendering (not `AugmentREADME`) for data available at generation time.
+
+## Key Technical Decisions
+
+- **Extend `research.json` rather than a separate sidecar file:** The skill writes research.json at Step 1.5e, which happens after transcendence features are identified (Steps 1.5c-1.5d). Adding `novel_features` to that same write is one field change, not a new file + new loader + new function. `json:",omitempty"` keeps backward compatibility. This follows the recommendation from `docs/plans/2026-04-01-008` which established research.json as the single sidecar contract.
+- **Duplicate type across packages (pipeline + generator):** Follows the existing `ReadmeSource` pattern. The `generator` package does not import `pipeline`, so a mirror type with field-by-field copy in `loadResearchSources()` is the established convention. Compile-time safety catches drift.
+- **Section heading "What's New Here" over "Novel Features" or "Beyond the Ecosystem":** Direct, non-marketing, answers the reader's question. The reader seeing this section immediately understands: these are things other tools don't have.
+- **No scores or evidence in README:** Scores and evidence citations are internal research artifacts. The README reader wants to know what the tool does, not how features were rated. The skill filters to >= 5/10 before writing to research.json.
+- **Cap at 5 features in the skill instruction:** A focused selection reads better than an exhaustive list. The skill orders by score descending and caps at 5.
+
+## Open Questions
+
+### Resolved During Planning
+
+- **Where to place the new section?** Between the Troubleshooting `---` divider and "Sources & Inspiration". After all practical sections, before credits. Does not interfere with the 5 scored sections.
+- **What if `RunResearch()` (automated path) runs and writes research.json without novel features?** `json:",omitempty"` means the field is simply absent. `loadResearchSources()` sees nil/empty, Generator gets nil, template guard skips the section. No behavioral change on the automated path.
+
+### Deferred to Implementation
+
+- **Exact template whitespace:** Go template whitespace trimming (`{{-` vs `{{`) needs to produce clean markdown with no extra blank lines. Will be tuned during implementation by reading the rendered output.
+
+## Implementation Units
+
+- [x] **Unit 1: Add `NovelFeature` type and extend `ResearchResult`**
+
+**Goal:** Make research.json capable of carrying novel features.
+
+**Requirements:** R4
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/pipeline/research.go`
+- Test: `internal/pipeline/research_test.go`
+
+**Approach:**
+- Add `NovelFeature` struct with `Name`, `Command`, `Description`, `Rationale` (all string, all json-tagged)
+- Add `NovelFeatures []NovelFeature` field to `ResearchResult` with `json:"novel_features,omitempty"`
+- No separate `NovelFeaturesForREADME()` helper needed -- unlike `SourcesForREADME()` which filters empty URLs and sorts by stars, novel features require no transformation. The caller in `loadResearchSources()` accesses `research.NovelFeatures` directly with a nil guard, matching the `DiscoveryPages` pattern
+
+**Patterns to follow:**
+- `ReadmeSource` struct definition at line 173
+- `Alternative` struct with json tags at line 53
+
+**Test scenarios:**
+- Happy path: `TestWriteAndLoadResearch` extended -- write a `ResearchResult` with 2 `NovelFeatures`, load it back, assert fields round-trip correctly
+- Empty: write `ResearchResult` with nil `NovelFeatures`, load back, assert `NovelFeatures` is nil (omitempty works)
+
+**Verification:**
+- `go test ./internal/pipeline/...` passes
+- research.json with novel_features round-trips through write/load
+
+- [x] **Unit 2: Carry novel features through the Generator to the template**
+
+**Goal:** Make the README template able to access novel features.
+
+**Requirements:** R1, R2, R4
+
+**Dependencies:** Unit 1
+
+**Files:**
+- Modify: `internal/generator/generator.go`
+- Modify: `internal/cli/root.go`
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Add `NovelFeature` type in generator package (mirrors pipeline type: `Name`, `Command`, `Description`, `Rationale`)
+- Add `NovelFeatures []NovelFeature` field to `Generator` struct
+- Add `NovelFeatures []NovelFeature` field to `readmeTemplateData` struct
+- Pass through in `readmeData()` method
+- Extend `loadResearchSources()` in root.go: inside the existing `if err == nil` block (where `research` is non-nil), iterate `research.NovelFeatures`, convert each to `generator.NovelFeature`, assign to `gen.NovelFeatures`
+
+**Patterns to follow:**
+- `generator.ReadmeSource` type at line 29 (mirrors pipeline type)
+- `Generator.Sources` field at line 41
+- `readmeTemplateData.Sources` at line 240
+- `readmeData()` pass-through at line 253
+- `loadResearchSources()` conversion loop at lines 684-692
+
+**Test scenarios:**
+- Happy path: set `gen.NovelFeatures` to 2 entries, call `Generate()`, read README.md, assert "What's New Here" heading appears, assert both feature commands appear as `###` headings, assert descriptions and rationales appear
+- Absent: leave `gen.NovelFeatures` nil, call `Generate()`, assert README does not contain "What's New Here"
+- Edge case: single novel feature still renders the section (unlike Sources which requires 2+)
+- Ordering: if both "What's New Here" and "Sources & Inspiration" are present, assert "What's New Here" appears first using `strings.Index` comparison
+
+**Verification:**
+- `go test ./internal/generator/...` passes
+- No cli-level test needed for `loadResearchSources` -- the conversion is covered indirectly by the generator-level happy/absent tests above
+
+- [x] **Unit 3: Add "What's New Here" section to the README template**
+
+**Goal:** Render transcendence features in the generated README when present.
+
+**Requirements:** R1, R2, R3, R5
+
+**Dependencies:** Unit 2 (template data must be available)
+
+**Files:**
+- Modify: `internal/generator/templates/readme.md.tmpl`
+
+**Approach:**
+- Insert a conditional block between the `---` divider (line 209) and the Sources & Inspiration conditional (line 210)
+- Guard with `{{- if .NovelFeatures}}`
+- Each feature rendered as a `###` heading with the full CLI command, description as body text, rationale as a blockquote
+- Section is completely absent when `NovelFeatures` is nil or empty
+
+**Patterns to follow:**
+- Sources & Inspiration conditional at lines 210-227 (conditional section with `{{- if}}` guard)
+- Resource command rendering pattern at lines 107-114 (range over collection, use `$.Name` for CLI name)
+
+**Test scenarios:**
+- Test expectation: covered by Unit 2's generator tests (template rendering is tested by calling `gen.Generate()` and reading the output file)
+
+**Verification:**
+- Generated README with novel features contains the "What's New Here" heading, command subheadings, descriptions, and rationale blockquotes
+- Generated README without novel features has no trace of the section
+- All 5 scored sections (Quick Start, Agent Usage, Health Check, Troubleshooting, Cookbook) remain present
+
+- [x] **Unit 4: Update skill instructions to write novel features to research.json**
+
+**Goal:** Have the skill populate the `novel_features` field when writing research.json at Step 1.5e.
+
+**Requirements:** R1, R4
+
+**Dependencies:** Unit 1 (the struct must accept the field)
+
+**Files:**
+- Modify: `skills/printing-press/SKILL.md`
+
+**Approach:**
+- Extend the Step 1.5e heredoc example to include a `"novel_features"` array
+- Add rules below the example: only features scoring >= 5/10, cap at 5 ordered by score descending, user-facing descriptions (not implementation details), skip the field entirely if no features qualify
+- The `description` field should be user-benefit language, the `rationale` field should explain why this is only possible with our approach
+- The `command` field must match an actual CLI subcommand that will be built in Phase 3
+
+**Patterns to follow:**
+- Step 1.5e's existing heredoc and rules for research.json at lines 815-843
+
+**Test scenarios:**
+- Test expectation: none -- skill instruction change, not code. Validated by next printing-press run.
+
+**Verification:**
+- Skill SKILL.md contains the updated Step 1.5e with novel_features in the JSON example
+- The JSON example matches the `ResearchResult` struct contract (field name `novel_features`, array of objects with `name`, `command`, `description`, `rationale`)
+
+## System-Wide Impact
+
+- **Interaction graph:** `loadResearchSources()` is the sole bridge between research data and the generator. No other callers need changes. The skill writes the data; the generator reads it. No callbacks, no middleware.
+- **Error propagation:** `LoadResearch()` already returns errors that `loadResearchSources` silently skips (bare `if err == nil` guard, no stderr warning). Novel features ride the same path -- if research.json is malformed, the entire Sources + NovelFeatures loading is silently skipped, producing empty sections in the README.
+- **API surface parity:** The automated `RunResearch()` path does not produce novel features. This is correct -- only the skill-driven absorb phase invents features. No change needed to `RunResearch()`.
+- **Unchanged invariants:** The 5 scored README sections, the Sources & Inspiration section, the `--research-dir` flag contract, the `AugmentREADME` marker system, and the quality gates are all unaffected.
+
+## Risks & Dependencies
+
+| Risk | Mitigation |
+|------|------------|
+| Skill writes malformed novel_features JSON | `json:",omitempty"` + graceful nil handling means the README section simply doesn't appear. Same resilience as existing Sources. |
+| Command name in novel_features diverges from actual built command | The skill writes novel_features at Step 1.5e and builds the commands in Phase 3. Names may drift. Acceptable: the LLM polish pass or manual review catches this. |
+| Template whitespace produces extra blank lines | Tune `{{-` trimming during implementation; verify by reading rendered README. |
+
+## Sources & References
+
+- Predecessor plan: `docs/plans/2026-04-01-008-feat-readme-source-credits-plan.md`
+- Absorb & Transcend philosophy: `docs/plans/2026-03-28-feat-skill-landscape-absorb-transcend-plan.md`
+- Auto-brainstorm scoring: `docs/plans/2026-03-30-009-feat-auto-brainstorm-before-absorb-gate-plan.md`
+- Scored README sections retro: `docs/retros/2026-04-01-steam-run4-retro.md`
+- Output layout contract: `docs/solutions/best-practices/checkout-scoped-printing-press-output-layout-2026-03-28.md`
diff --git a/internal/cli/dogfood.go b/internal/cli/dogfood.go
index a156a29b..16495408 100644
--- a/internal/cli/dogfood.go
+++ b/internal/cli/dogfood.go
@@ -14,6 +14,7 @@ import (
func newDogfoodCmd() *cobra.Command {
var dir string
var specPath string
+ var researchDir string
var asJSON bool
cmd := &cobra.Command{
@@ -26,7 +27,11 @@ func newDogfoodCmd() *cobra.Command {
# Output as JSON for programmatic use
printing-press dogfood --dir ./generated/stripe-pp-cli --json`,
RunE: func(cmd *cobra.Command, args []string) error {
- report, err := pipeline.RunDogfood(dir, specPath)
+ var opts []pipeline.DogfoodOption
+ if researchDir != "" {
+ opts = append(opts, pipeline.WithResearchDir(researchDir))
+ }
+ report, err := pipeline.RunDogfood(dir, specPath, opts...)
if err != nil {
return &ExitError{Code: ExitGenerationError, Err: fmt.Errorf("running dogfood: %w", err)}
}
@@ -44,6 +49,7 @@ func newDogfoodCmd() *cobra.Command {
cmd.Flags().StringVar(&dir, "dir", "", "Path to the generated CLI directory (required)")
cmd.Flags().StringVar(&specPath, "spec", "", "Path to the OpenAPI spec file")
+ cmd.Flags().StringVar(&researchDir, "research-dir", "", "Pipeline directory containing research.json for novel features validation")
cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
_ = cmd.MarkFlagRequired("dir")
return cmd
@@ -150,6 +156,23 @@ func printDogfoodReport(report *pipeline.DogfoodReport) {
}
fmt.Println()
+ nfc := report.NovelFeaturesCheck
+ if nfc.Skipped {
+ fmt.Println("Novel Features: SKIP (no research.json)")
+ } else if nfc.Planned == 0 {
+ fmt.Println("Novel Features: SKIP (none planned)")
+ } else {
+ nfStatus := "PASS"
+ if len(nfc.Missing) > 0 {
+ nfStatus = "WARN"
+ }
+ fmt.Printf("Novel Features: %d/%d survived (%s)\n", nfc.Found, nfc.Planned, nfStatus)
+ for _, cmd := range nfc.Missing {
+ fmt.Printf(" - %s: planned but not found\n", cmd)
+ }
+ }
+ fmt.Println()
+
fmt.Printf("Verdict: %s\n", report.Verdict)
for _, issue := range report.Issues {
fmt.Printf(" - %s\n", issue)
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 25fc23d9..63474be2 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -673,9 +673,9 @@ func printDryRun(apiSpec *spec.APISpec, absOut string, specFiles []string) error
return enc.Encode(summary)
}
-// loadResearchSources populates the generator's Sources and DiscoveryPages
-// from a pipeline research directory. Silently skips if researchDir is empty
-// or data is unavailable.
+// loadResearchSources populates the generator's Sources, DiscoveryPages, and
+// NovelFeatures from a pipeline research directory. Silently skips if
+// researchDir is empty or data is unavailable.
func loadResearchSources(gen *generator.Generator, researchDir string) {
if researchDir == "" {
return
@@ -690,6 +690,25 @@ func loadResearchSources(gen *generator.Generator, researchDir string) {
Stars: s.Stars,
})
}
+ // Prefer verified (built) novel features over the aspirational list.
+ // novel_features_built is written by dogfood after validating which
+ // planned features actually survived the build. A nil pointer means
+ // dogfood hasn't run yet (fall back to planned). A non-nil pointer
+ // to an empty slice means dogfood ran and nothing survived (show nothing).
+ var novelSrc []pipeline.NovelFeature
+ if research.NovelFeaturesBuilt != nil {
+ novelSrc = *research.NovelFeaturesBuilt
+ } else {
+ novelSrc = research.NovelFeatures
+ }
+ for _, nf := range novelSrc {
+ gen.NovelFeatures = append(gen.NovelFeatures, generator.NovelFeature{
+ Name: nf.Name,
+ Command: nf.Command,
+ Description: nf.Description,
+ Rationale: nf.Rationale,
+ })
+ }
}
discoveryDir := filepath.Join(researchDir, "discovery")
gen.DiscoveryPages = pipeline.ParseDiscoveryPages(discoveryDir)
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index b6de8a32..9c39c1e6 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -33,6 +33,14 @@ type ReadmeSource struct {
Stars int
}
+// NovelFeature represents a transcendence feature for the README.
+type NovelFeature struct {
+ Name string
+ Command string
+ Description string
+ Rationale string
+}
+
type Generator struct {
Spec *spec.APISpec
OutputDir string
@@ -40,6 +48,7 @@ type Generator struct {
FixtureSet *websniff.FixtureSet
Sources []ReadmeSource // Ecosystem tools to credit in README
DiscoveryPages []string // Pages visited during sniff discovery
+ NovelFeatures []NovelFeature // Transcendence features for README
profile *profiler.APIProfile
funcs template.FuncMap
templates map[string]*template.Template
@@ -239,6 +248,7 @@ type readmeTemplateData struct {
*spec.APISpec
Sources []ReadmeSource
DiscoveryPages []string
+ NovelFeatures []NovelFeature
}
func (g *Generator) readmeData() *readmeTemplateData {
@@ -252,6 +262,7 @@ func (g *Generator) readmeData() *readmeTemplateData {
APISpec: g.Spec,
Sources: g.Sources,
DiscoveryPages: g.DiscoveryPages,
+ NovelFeatures: g.NovelFeatures,
}
}
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 1f5af759..dab15d5a 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -409,6 +409,85 @@ func TestGeneratedOutput_READMESourcesSection(t *testing.T) {
})
}
+func TestGeneratedOutput_READMENovelFeaturesSection(t *testing.T) {
+ t.Parallel()
+
+ minSpec := &spec.APISpec{
+ Name: "testapi",
+ Version: "0.1.0",
+ BaseURL: "https://api.example.com",
+ Auth: spec.AuthConfig{Type: "api_key", Header: "X-Api-Key", EnvVars: []string{"TESTAPI_API_KEY"}},
+ Config: spec.ConfigSpec{Format: "toml", Path: "~/.config/testapi-pp-cli/config.toml"},
+ Resources: map[string]spec.Resource{
+ "items": {Description: "Items", Endpoints: map[string]spec.Endpoint{
+ "list": {Method: "GET", Path: "/items", Description: "List items"},
+ }},
+ },
+ }
+
+ t.Run("section appears with novel features", func(t *testing.T) {
+ outputDir := filepath.Join(t.TempDir(), "testapi-pp-cli")
+ gen := New(minSpec, outputDir)
+ gen.NovelFeatures = []NovelFeature{
+ {Name: "Health dashboard", Command: "health", Description: "See scheduling health metrics at a glance", Rationale: "Requires correlating bookings and schedules in the local store"},
+ {Name: "Stale triage", Command: "triage", Description: "Find unconfirmed bookings older than N days", Rationale: "No existing tool offers batch triage"},
+ }
+ require.NoError(t, gen.Generate())
+
+ readme, err := os.ReadFile(filepath.Join(outputDir, "README.md"))
+ require.NoError(t, err)
+ content := string(readme)
+ assert.Contains(t, content, "## What's New Here")
+ assert.Contains(t, content, "### `testapi-pp-cli health`")
+ assert.Contains(t, content, "### `testapi-pp-cli triage`")
+ assert.Contains(t, content, "See scheduling health metrics at a glance")
+ assert.Contains(t, content, "> Requires correlating bookings and schedules in the local store")
+ })
+
+ t.Run("section absent with no novel features", func(t *testing.T) {
+ outputDir := filepath.Join(t.TempDir(), "testapi-pp-cli")
+ gen := New(minSpec, outputDir)
+ require.NoError(t, gen.Generate())
+
+ readme, err := os.ReadFile(filepath.Join(outputDir, "README.md"))
+ require.NoError(t, err)
+ assert.NotContains(t, string(readme), "What's New Here")
+ })
+
+ t.Run("single novel feature still renders section", func(t *testing.T) {
+ outputDir := filepath.Join(t.TempDir(), "testapi-pp-cli")
+ gen := New(minSpec, outputDir)
+ gen.NovelFeatures = []NovelFeature{
+ {Name: "Health dashboard", Command: "health", Description: "Metrics at a glance", Rationale: "Local data only"},
+ }
+ require.NoError(t, gen.Generate())
+
+ readme, err := os.ReadFile(filepath.Join(outputDir, "README.md"))
+ require.NoError(t, err)
+ assert.Contains(t, string(readme), "## What's New Here")
+ })
+
+ t.Run("novel features appear before sources", func(t *testing.T) {
+ outputDir := filepath.Join(t.TempDir(), "testapi-pp-cli")
+ gen := New(minSpec, outputDir)
+ gen.NovelFeatures = []NovelFeature{
+ {Name: "Health dashboard", Command: "health", Description: "Metrics", Rationale: "Local data"},
+ }
+ gen.Sources = []ReadmeSource{
+ {Name: "a", URL: "https://github.com/org/a", Stars: 100},
+ {Name: "b", URL: "https://github.com/org/b", Stars: 50},
+ }
+ require.NoError(t, gen.Generate())
+
+ readme, err := os.ReadFile(filepath.Join(outputDir, "README.md"))
+ require.NoError(t, err)
+ content := string(readme)
+ novelIdx := strings.Index(content, "What's New Here")
+ sourcesIdx := strings.Index(content, "Sources & Inspiration")
+ assert.Greater(t, sourcesIdx, novelIdx, "Novel features should appear before Sources")
+ })
+}
+
func TestGeneratedOutput_MutatingCommandsHaveEnvelope(t *testing.T) {
t.Parallel()
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index c5636814..34c7b057 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -207,6 +207,19 @@ Environment variables:
- If persistent, wait a few minutes and try again
---
+{{- if .NovelFeatures}}
+
+## What's New Here
+
+These capabilities don't exist in any other tool for this API.
+{{range .NovelFeatures}}
+### `{{$.Name}}-pp-cli {{.Command}}`
+
+{{.Description}}
+
+> {{.Rationale}}
+{{end}}
+{{- end}}
{{- if or (gt (len .Sources) 1) (gt (len .DiscoveryPages) 0)}}
## Sources & Inspiration
diff --git a/internal/pipeline/dogfood.go b/internal/pipeline/dogfood.go
index 88329148..71e5dde6 100644
--- a/internal/pipeline/dogfood.go
+++ b/internal/pipeline/dogfood.go
@@ -19,17 +19,27 @@ import (
)
type DogfoodReport struct {
- Dir string `json:"dir"`
- SpecPath string `json:"spec_path,omitempty"`
- Verdict string `json:"verdict"`
- PathCheck PathCheckResult `json:"path_check"`
- AuthCheck AuthCheckResult `json:"auth_check"`
- DeadFlags DeadCodeResult `json:"dead_flags"`
- DeadFuncs DeadCodeResult `json:"dead_functions"`
- PipelineCheck PipelineResult `json:"pipeline_check"`
- ExampleCheck ExampleCheckResult `json:"example_check"`
- WiringCheck WiringCheckResult `json:"wiring_check"`
- Issues []string `json:"issues"`
+ Dir string `json:"dir"`
+ SpecPath string `json:"spec_path,omitempty"`
+ Verdict string `json:"verdict"`
+ PathCheck PathCheckResult `json:"path_check"`
+ AuthCheck AuthCheckResult `json:"auth_check"`
+ DeadFlags DeadCodeResult `json:"dead_flags"`
+ DeadFuncs DeadCodeResult `json:"dead_functions"`
+ PipelineCheck PipelineResult `json:"pipeline_check"`
+ ExampleCheck ExampleCheckResult `json:"example_check"`
+ WiringCheck WiringCheckResult `json:"wiring_check"`
+ NovelFeaturesCheck NovelFeaturesCheckResult `json:"novel_features_check"`
+ Issues []string `json:"issues"`
+}
+
+// NovelFeaturesCheckResult tracks whether transcendence features planned
+// during absorb actually survived the build as registered CLI commands.
+type NovelFeaturesCheckResult struct {
+ Planned int `json:"planned"`
+ Found int `json:"found"`
+ Missing []string `json:"missing,omitempty"`
+ Skipped bool `json:"skipped,omitempty"`
}
type PathCheckResult struct {
@@ -101,7 +111,12 @@ type openAPISpec struct {
Auth apispec.AuthConfig
}
-func RunDogfood(dir, specPath string) (*DogfoodReport, error) {
+func RunDogfood(dir, specPath string, opts ...DogfoodOption) (*DogfoodReport, error) {
+ cfg := dogfoodConfig{}
+ for _, o := range opts {
+ o(&cfg)
+ }
+
report := &DogfoodReport{
Dir: dir,
SpecPath: specPath,
@@ -130,6 +145,7 @@ func RunDogfood(dir, specPath string) (*DogfoodReport, error) {
report.PipelineCheck = checkPipelineIntegrity(dir)
report.ExampleCheck = checkExamples(dir)
report.WiringCheck = checkWiring(dir)
+ report.NovelFeaturesCheck = checkNovelFeatures(dir, cfg.researchDir)
report.Issues = collectDogfoodIssues(report, spec != nil)
report.Verdict = deriveDogfoodVerdict(report, spec != nil)
@@ -140,6 +156,85 @@ func RunDogfood(dir, specPath string) (*DogfoodReport, error) {
return report, nil
}
+type dogfoodConfig struct {
+ researchDir string
+}
+
+// DogfoodOption configures optional behavior for RunDogfood.
+type DogfoodOption func(*dogfoodConfig)
+
+// WithResearchDir provides the pipeline directory containing research.json
+// so dogfood can validate novel features against registered commands.
+func WithResearchDir(dir string) DogfoodOption {
+ return func(c *dogfoodConfig) {
+ c.researchDir = dir
+ }
+}
+
+// checkNovelFeatures validates that transcendence features from research.json
+// have corresponding registered commands in the generated CLI. It also writes
+// the verified list back as novel_features_built so downstream consumers
+// (README, publish) only claim what actually exists.
+func checkNovelFeatures(cliDir, researchDir string) NovelFeaturesCheckResult {
+ if researchDir == "" {
+ return NovelFeaturesCheckResult{Skipped: true}
+ }
+ research, err := LoadResearch(researchDir)
+ if err != nil || len(research.NovelFeatures) == 0 {
+ return NovelFeaturesCheckResult{Skipped: true}
+ }
+
+ // Build set of registered command Use: names from the CLI source.
+ registeredCmds := collectRegisteredCommands(cliDir)
+
+ result := NovelFeaturesCheckResult{
+ Planned: len(research.NovelFeatures),
+ }
+ built := make([]NovelFeature, 0)
+ for _, nf := range research.NovelFeatures {
+ // The command field may be a subcommand path like "issues stale".
+ // Check if the leaf command name is registered.
+ parts := strings.Fields(nf.Command)
+ leaf := parts[len(parts)-1]
+ if registeredCmds[leaf] {
+ result.Found++
+ built = append(built, nf)
+ } else {
+ result.Missing = append(result.Missing, nf.Command)
+ }
+ }
+
+ // Write the verified list back to research.json so the README and
+ // publish steps only reference features that actually exist.
+ if err := WriteNovelFeaturesBuilt(researchDir, built); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: could not write novel_features_built: %v\n", err)
+ }
+
+ return result
+}
+
+// collectRegisteredCommands returns a set of cobra Use: names found in the
+// CLI's internal/cli/*.go files.
+func collectRegisteredCommands(dir string) map[string]bool {
+ cliDir := filepath.Join(dir, "internal", "cli")
+ files := listGoFiles(cliDir)
+ useFieldRe := regexp.MustCompile(`(?m)Use:\s*"([^"\s]+)`)
+ cmds := make(map[string]bool)
+ for _, file := range files {
+ data, err := os.ReadFile(file)
+ if err != nil {
+ continue
+ }
+ for _, m := range useFieldRe.FindAllStringSubmatch(string(data), -1) {
+ name := strings.Fields(m[1])[0]
+ if name != "" {
+ cmds[name] = true
+ }
+ }
+ }
+ return cmds
+}
+
func LoadDogfoodResults(dir string) (*DogfoodReport, error) {
data, err := os.ReadFile(filepath.Join(dir, "dogfood-results.json"))
if err != nil {
@@ -588,6 +683,9 @@ func deriveDogfoodVerdict(report *DogfoodReport, hasSpec bool) string {
if len(report.WiringCheck.WorkflowComplete.UnmappedSteps) > 0 {
return "WARN"
}
+ if len(report.NovelFeaturesCheck.Missing) > 0 {
+ return "WARN"
+ }
return "PASS"
}
@@ -634,6 +732,12 @@ func collectDogfoodIssues(report *DogfoodReport, hasSpec bool) []string {
len(report.WiringCheck.WorkflowComplete.UnmappedSteps),
strings.Join(report.WiringCheck.WorkflowComplete.UnmappedSteps, ", ")))
}
+ if len(report.NovelFeaturesCheck.Missing) > 0 {
+ issues = append(issues, fmt.Sprintf("%d/%d novel features missing: %s",
+ len(report.NovelFeaturesCheck.Missing),
+ report.NovelFeaturesCheck.Planned,
+ strings.Join(report.NovelFeaturesCheck.Missing, ", ")))
+ }
return issues
}
diff --git a/internal/pipeline/dogfood_test.go b/internal/pipeline/dogfood_test.go
index a37e620d..c221d8e1 100644
--- a/internal/pipeline/dogfood_test.go
+++ b/internal/pipeline/dogfood_test.go
@@ -591,6 +591,156 @@ func TestDeriveDogfoodVerdict_WiringChecks(t *testing.T) {
assert.Equal(t, "PASS", deriveDogfoodVerdict(report, true))
}
+func TestCheckNovelFeatures(t *testing.T) {
+ t.Run("skipped when no research dir", func(t *testing.T) {
+ result := checkNovelFeatures(t.TempDir(), "")
+ assert.True(t, result.Skipped)
+ })
+
+ t.Run("skipped when no novel features in research", func(t *testing.T) {
+ researchDir := t.TempDir()
+ research := &ResearchResult{APIName: "test", NoveltyScore: 5}
+ require.NoError(t, writeResearchJSON(research, researchDir))
+ result := checkNovelFeatures(t.TempDir(), researchDir)
+ assert.True(t, result.Skipped)
+ })
+
+ t.Run("finds matching commands", func(t *testing.T) {
+ // Set up a CLI dir with a command file
+ cliDir := t.TempDir()
+ cliCodeDir := filepath.Join(cliDir, "internal", "cli")
+ require.NoError(t, os.MkdirAll(cliCodeDir, 0o755))
+ writeTestFile(t, filepath.Join(cliCodeDir, "health.go"),
+ `package cli
+func newHealthCmd() *cobra.Command {
+ return &cobra.Command{Use: "health"}
+}`)
+ writeTestFile(t, filepath.Join(cliCodeDir, "triage.go"),
+ `package cli
+func newTriageCmd() *cobra.Command {
+ return &cobra.Command{Use: "triage"}
+}`)
+
+ // Set up research with novel features
+ researchDir := t.TempDir()
+ research := &ResearchResult{
+ APIName: "test",
+ NovelFeatures: []NovelFeature{
+ {Name: "Health dashboard", Command: "health"},
+ {Name: "Stale triage", Command: "triage"},
+ },
+ }
+ require.NoError(t, writeResearchJSON(research, researchDir))
+
+ result := checkNovelFeatures(cliDir, researchDir)
+ assert.False(t, result.Skipped)
+ assert.Equal(t, 2, result.Planned)
+ assert.Equal(t, 2, result.Found)
+ assert.Empty(t, result.Missing)
+
+ // Verify novel_features_built was written back
+ updated, err := LoadResearch(researchDir)
+ require.NoError(t, err)
+ assert.Len(t, updated.NovelFeatures, 2, "planned list preserved")
+ require.NotNil(t, updated.NovelFeaturesBuilt)
+ assert.Len(t, *updated.NovelFeaturesBuilt, 2, "all built")
+ })
+
+ t.Run("detects missing commands and writes verified subset", func(t *testing.T) {
+ cliDir := t.TempDir()
+ cliCodeDir := filepath.Join(cliDir, "internal", "cli")
+ require.NoError(t, os.MkdirAll(cliCodeDir, 0o755))
+ writeTestFile(t, filepath.Join(cliCodeDir, "health.go"),
+ `package cli
+func newHealthCmd() *cobra.Command {
+ return &cobra.Command{Use: "health"}
+}`)
+
+ researchDir := t.TempDir()
+ research := &ResearchResult{
+ APIName: "test",
+ NovelFeatures: []NovelFeature{
+ {Name: "Health dashboard", Command: "health"},
+ {Name: "Stale triage", Command: "triage"},
+ {Name: "Team util", Command: "team utilization"},
+ },
+ }
+ require.NoError(t, writeResearchJSON(research, researchDir))
+
+ result := checkNovelFeatures(cliDir, researchDir)
+ assert.Equal(t, 3, result.Planned)
+ assert.Equal(t, 1, result.Found)
+ assert.Equal(t, []string{"triage", "team utilization"}, result.Missing)
+
+ // Verify novel_features_built contains only the survivor
+ updated, err := LoadResearch(researchDir)
+ require.NoError(t, err)
+ assert.Len(t, updated.NovelFeatures, 3, "planned list preserved")
+ require.NotNil(t, updated.NovelFeaturesBuilt)
+ require.Len(t, *updated.NovelFeaturesBuilt, 1, "only health survived")
+ assert.Equal(t, "health", (*updated.NovelFeaturesBuilt)[0].Command)
+ })
+}
+
+func TestCheckNovelFeatures_ZeroSurvivors(t *testing.T) {
+ // All planned features missing — novel_features_built should be a non-nil
+ // empty slice (not omitted), so the fallback to the aspirational list
+ // does NOT kick in.
+ cliDir := t.TempDir()
+ cliCodeDir := filepath.Join(cliDir, "internal", "cli")
+ require.NoError(t, os.MkdirAll(cliCodeDir, 0o755))
+ // No command files — nothing registered
+
+ researchDir := t.TempDir()
+ research := &ResearchResult{
+ APIName: "test",
+ NovelFeatures: []NovelFeature{
+ {Name: "Health", Command: "health"},
+ {Name: "Triage", Command: "triage"},
+ },
+ }
+ require.NoError(t, writeResearchJSON(research, researchDir))
+
+ result := checkNovelFeatures(cliDir, researchDir)
+ assert.Equal(t, 2, result.Planned)
+ assert.Equal(t, 0, result.Found)
+ assert.Len(t, result.Missing, 2)
+
+ // Verify research.json has novel_features_built as non-nil empty
+ updated, err := LoadResearch(researchDir)
+ require.NoError(t, err)
+ assert.Len(t, updated.NovelFeatures, 2, "planned list preserved")
+ require.NotNil(t, updated.NovelFeaturesBuilt, "must be non-nil so fallback doesn't kick in")
+ assert.Empty(t, *updated.NovelFeaturesBuilt, "empty — nothing survived")
+}
+
+func TestDeriveDogfoodVerdict_NovelFeatures(t *testing.T) {
+ base := &DogfoodReport{
+ PathCheck: PathCheckResult{Tested: 10, Valid: 10, Pct: 100},
+ AuthCheck: AuthCheckResult{Match: true},
+ DeadFlags: DeadCodeResult{Dead: 0},
+ DeadFuncs: DeadCodeResult{Dead: 0},
+ PipelineCheck: PipelineResult{SyncCallsDomain: true},
+ WiringCheck: WiringCheckResult{
+ CommandTree: CommandTreeResult{Defined: 2, Registered: 2},
+ ConfigConsist: ConfigConsistResult{Consistent: true},
+ WorkflowComplete: WorkflowCompleteResult{Skipped: true},
+ },
+ }
+
+ // Missing novel features → WARN
+ base.NovelFeaturesCheck = NovelFeaturesCheckResult{Planned: 3, Found: 1, Missing: []string{"triage", "utilization"}}
+ assert.Equal(t, "WARN", deriveDogfoodVerdict(base, true))
+
+ // All found → PASS
+ base.NovelFeaturesCheck = NovelFeaturesCheckResult{Planned: 2, Found: 2}
+ assert.Equal(t, "PASS", deriveDogfoodVerdict(base, true))
+
+ // Skipped → PASS (no penalty)
+ base.NovelFeaturesCheck = NovelFeaturesCheckResult{Skipped: true}
+ assert.Equal(t, "PASS", deriveDogfoodVerdict(base, true))
+}
+
func writeTestFile(t *testing.T, path string, content string) {
t.Helper()
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
diff --git a/internal/pipeline/fullrun.go b/internal/pipeline/fullrun.go
index 16358a2c..81ef6ac4 100644
--- a/internal/pipeline/fullrun.go
+++ b/internal/pipeline/fullrun.go
@@ -191,7 +191,7 @@ func MakeBestCLI(apiName, level, specFlag, specURL, outputDir, pressBinary strin
result.Errors = append(result.Errors, fmt.Sprintf("dogfood build: %v", buildErr))
} else {
defer func() { _ = os.Remove(cliBinaryPath) }()
- dogfood, dogErr := RunDogfood(workingDir, qualitySpecPath)
+ dogfood, dogErr := RunDogfood(workingDir, qualitySpecPath, WithResearchDir(researchDir))
if dogErr != nil {
result.DogfoodError = dogErr.Error()
result.Errors = append(result.Errors, fmt.Sprintf("dogfood: %v", dogErr))
diff --git a/internal/pipeline/research.go b/internal/pipeline/research.go
index 2a8fc91c..186e5eb7 100644
--- a/internal/pipeline/research.go
+++ b/internal/pipeline/research.go
@@ -29,6 +29,20 @@ type ResearchResult struct {
Recommendation string `json:"recommendation"` // "proceed", "proceed-with-gaps", "skip"
ResearchedAt time.Time `json:"researched_at"`
CompetitorInsights *CompetitorInsights `json:"competitor_insights,omitempty"`
+ NovelFeatures []NovelFeature `json:"novel_features,omitempty"`
+ // NovelFeaturesBuilt is the verified subset written by dogfood. Intentionally
+ // NOT omitempty: an empty [] means "dogfood ran, nothing survived" while a
+ // missing field means "dogfood hasn't validated yet."
+ NovelFeaturesBuilt *[]NovelFeature `json:"novel_features_built,omitempty"`
+}
+
+// NovelFeature represents a transcendence feature invented during the absorb
+// phase — a capability not found in any existing tool for this API.
+type NovelFeature struct {
+ Name string `json:"name"`
+ Command string `json:"command"`
+ Description string `json:"description"`
+ Rationale string `json:"rationale"`
}
// CompetitorAnalysis holds intelligence gathered from a single competitor repo.
@@ -169,6 +183,19 @@ func LoadResearch(pipelineDir string) (*ResearchResult, error) {
return &r, nil
}
+// WriteNovelFeaturesBuilt updates research.json with the verified list of
+// novel features that survived the build. The original novel_features field
+// is preserved as-is (the planned list); novel_features_built records what
+// actually exists in the CLI.
+func WriteNovelFeaturesBuilt(pipelineDir string, built []NovelFeature) error {
+ research, err := LoadResearch(pipelineDir)
+ if err != nil {
+ return err
+ }
+ research.NovelFeaturesBuilt = &built
+ return writeResearchJSON(research, pipelineDir)
+}
+
// ReadmeSource represents a credited ecosystem tool for the generated README.
type ReadmeSource struct {
Name string
diff --git a/internal/pipeline/research_test.go b/internal/pipeline/research_test.go
index b23aeb4b..80412c04 100644
--- a/internal/pipeline/research_test.go
+++ b/internal/pipeline/research_test.go
@@ -233,6 +233,84 @@ func TestWriteAndLoadResearchWithCompetitorInsights(t *testing.T) {
assert.Equal(t, []string{"JSON output"}, loaded.CompetitorInsights.UnmetFeatures)
}
+func TestWriteAndLoadResearchWithNovelFeatures(t *testing.T) {
+ dir := t.TempDir()
+ result := &ResearchResult{
+ APIName: "test-api",
+ NoveltyScore: 8,
+ Recommendation: "proceed",
+ NovelFeatures: []NovelFeature{
+ {
+ Name: "Health dashboard",
+ Command: "health",
+ Description: "See scheduling health metrics at a glance",
+ Rationale: "Requires correlating bookings, schedules, and staff data in the local store",
+ },
+ {
+ Name: "Stale booking triage",
+ Command: "triage",
+ Description: "Find and act on unconfirmed bookings older than N days",
+ Rationale: "No existing tool offers batch triage of pending bookings",
+ },
+ },
+ }
+
+ err := writeResearchJSON(result, dir)
+ require.NoError(t, err)
+
+ loaded, err := LoadResearch(dir)
+ require.NoError(t, err)
+ require.Len(t, loaded.NovelFeatures, 2)
+ assert.Equal(t, "Health dashboard", loaded.NovelFeatures[0].Name)
+ assert.Equal(t, "health", loaded.NovelFeatures[0].Command)
+ assert.Equal(t, "See scheduling health metrics at a glance", loaded.NovelFeatures[0].Description)
+ assert.Equal(t, "Requires correlating bookings, schedules, and staff data in the local store", loaded.NovelFeatures[0].Rationale)
+ assert.Equal(t, "Stale booking triage", loaded.NovelFeatures[1].Name)
+}
+
+func TestWriteAndLoadResearchWithoutNovelFeatures(t *testing.T) {
+ dir := t.TempDir()
+ result := &ResearchResult{
+ APIName: "test-api",
+ NoveltyScore: 5,
+ Recommendation: "proceed",
+ }
+
+ err := writeResearchJSON(result, dir)
+ require.NoError(t, err)
+
+ loaded, err := LoadResearch(dir)
+ require.NoError(t, err)
+ assert.Nil(t, loaded.NovelFeatures)
+}
+
+func TestWriteNovelFeaturesBuilt(t *testing.T) {
+ dir := t.TempDir()
+ // Write initial research with planned features
+ result := &ResearchResult{
+ APIName: "test-api",
+ NovelFeatures: []NovelFeature{
+ {Name: "Health", Command: "health", Description: "Metrics", Rationale: "Local data"},
+ {Name: "Triage", Command: "triage", Description: "Find stale", Rationale: "Batch ops"},
+ },
+ }
+ require.NoError(t, writeResearchJSON(result, dir))
+
+ // Write verified subset
+ built := []NovelFeature{
+ {Name: "Health", Command: "health", Description: "Metrics", Rationale: "Local data"},
+ }
+ require.NoError(t, WriteNovelFeaturesBuilt(dir, built))
+
+ // Load and verify both lists are present
+ loaded, err := LoadResearch(dir)
+ require.NoError(t, err)
+ assert.Len(t, loaded.NovelFeatures, 2, "planned list preserved")
+ require.NotNil(t, loaded.NovelFeaturesBuilt)
+ assert.Len(t, *loaded.NovelFeaturesBuilt, 1, "built list is verified subset")
+ assert.Equal(t, "health", (*loaded.NovelFeaturesBuilt)[0].Command)
+}
+
func TestFetchIssuesWithMockServer(t *testing.T) {
issues := []ghIssue{
{Title: "Add dark mode", Body: "Please add dark mode support"},
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index c3f90634..60752b8e 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -830,6 +830,10 @@ cat > "$API_RUN_DIR/research.json" <<REOF
{"name": "<tool1>", "url": "<github-url>", "language": "<Go|JavaScript|Python|etc>", "stars": <N>, "command_count": <N>},
...
],
+ "novel_features": [
+ {"name": "<Feature Name>", "command": "<cli-subcommand>", "description": "<One sentence: what the user gets>", "rationale": "<One sentence: why only possible with our approach>"},
+ ...
+ ],
"gaps": [],
"patterns": [],
"recommendation": "proceed",
@@ -840,6 +844,14 @@ REOF
For each tool, fill in what you know from the research. Stars and command_count are optional (use 0 if unknown). The `language` field should match the primary implementation language. Skip tools that were found during search but contributed zero features to the manifest.
+**Novel features rules** (the `novel_features` array populates the README's "What's New Here" section):
+1. Only include transcendence features from the manifest that scored >= 5/10
+2. Cap at 5, ordered by score descending. A focused selection reads better than an exhaustive list.
+3. `description` should be user-benefit language, not implementation detail. Good: "See which team members are overloaded before sprint planning." Bad: "Requires local join across issues + assignees + cycle data."
+4. `rationale` should explain why this is only possible with our approach. Good: "Requires correlating bookings, schedules, and staff data that only exists together in the local store." Bad: "Cal.com Insights is paid-tier only."
+5. `command` must match the actual CLI subcommand that will be built in Phase 3. For subcommands of a resource (e.g., `issues stale`), use the full command path.
+6. If no transcendence features scored >= 5/10, omit the `novel_features` field entirely.
+
Also write discovery pages if sniff was used. The generator reads these from `$API_RUN_DIR/discovery/sniff-report.md` (which the sniff gate already writes there). No additional action needed for discovery pages -- they are already in the right location.
### Phase Gate 1.5
@@ -1250,14 +1262,14 @@ printing-press lock update --cli <api>-pp-cli --phase shipcheck
```
```bash
-printing-press dogfood --dir "$CLI_WORK_DIR" --spec <same-spec>
+printing-press dogfood --dir "$CLI_WORK_DIR" --spec <same-spec> --research-dir "$API_RUN_DIR"
printing-press verify --dir "$CLI_WORK_DIR" --spec <same-spec> --fix
printing-press workflow-verify --dir "$CLI_WORK_DIR"
printing-press scorecard --dir "$CLI_WORK_DIR" --spec <same-spec>
```
Interpretation:
-- `dogfood` catches dead flags, dead helpers, invalid paths, example drift, broken data wiring, and command tree/config field wiring bugs
+- `dogfood` catches dead flags, dead helpers, invalid paths, example drift, broken data wiring, command tree/config field wiring bugs, and novel features that were planned but not built
- `verify` catches runtime breakage and runs the auto-fix loop for common failures
- `workflow-verify` tests the primary workflow end-to-end using the verification manifest (workflow_verify.yaml). Three verdicts: workflow-pass, workflow-fail, unverified-needs-auth
- `scorecard` is the structural quality snapshot, not the source of truth by itself
@@ -1267,7 +1279,15 @@ Fix order (update heartbeat between each fix category to prevent stale lock duri
2. invalid paths and auth mismatches
3. dead flags / dead functions / ghost tables
4. broken dry-run and runtime command failures
-5. scorecard-only polish gaps
+5. missing novel features (see below)
+6. scorecard-only polish gaps
+
+**Missing novel features fix (step 5):** Dogfood writes `novel_features_built` to research.json — only features whose commands actually exist. The original `novel_features` (aspirational list from absorb) is preserved for the audit trail. After dogfood:
+
+1. If the README has a `## What's New Here` section from initial generation, update it to match `novel_features_built` — remove entries for commands that don't exist
+2. If `novel_features_built` is empty (none survived), remove the entire `## What's New Here` section from the README
+3. If the README doesn't have the section yet but `novel_features_built` is non-empty, inject it (same format as the template) before `## Sources & Inspiration` or the `Generated by` footer
+4. Log which features were dropped (planned vs built delta)
After fixing each category, update the heartbeat:
```bash
@@ -1482,6 +1502,7 @@ library repo. A run that isn't ready to publish still produces valuable research
CLI_NAME="$(basename "$PRESS_LIBRARY/<api>-pp-cli")"
mkdir -p "$PRESS_MANUSCRIPTS/$CLI_NAME/$RUN_ID"
cp -r "$RESEARCH_DIR" "$PRESS_MANUSCRIPTS/$CLI_NAME/$RUN_ID/research" 2>/dev/null || true
+cp -f "$API_RUN_DIR/research.json" "$PRESS_MANUSCRIPTS/$CLI_NAME/$RUN_ID/research.json" 2>/dev/null || true
cp -r "$PROOFS_DIR" "$PRESS_MANUSCRIPTS/$CLI_NAME/$RUN_ID/proofs" 2>/dev/null || true
# Archive discovery artifacts (sniff captures, URL lists, sniff report).
← 3ea8601d fix(skills): add Phase 3 Completion Gate to prevent skipping
·
back to Cli Printing Press
·
feat(cli): rename What's New Here to Unique Features, move a 1b4b9844 →