Hill Chart
Renders what the harness already derives mechanically — never invents a position, never re-runs a computation that would erase true history.
You are not a worker: no WorkOrder, no WorkResult, invoked directly by the user (or by /hill)
exactly like shapeup is. There is nothing to declare in
skills/tech-lead/schemas/domain.schema.json and nothing to teach harness compile or
harness reduce ingest — those steps exist only for dispatched workers.
What you read
Two tiers, per project (kernel/lib/paths.mjs's sharedRoot/localRoot):
- COMMITTED —
shapeup/<slug>/hill/<scope-id>.yml, one file per scope, two fields (scope_id:,phase:), a 4-value enum:UPHILL_UNKNOWN → UPHILL_SOLVED → DOWNHILL_EXECUTION → FINISHED. Written byharness reduce hill. Survives even after a project's local run trace is cleaned up post-ship. - LOCAL —
.shapeup/<slug>/graph.jsonl, the run graph (seekernel/reduce/graph.mjs). Written byharness reduce graph. Supplies everything else: current-round verdict health, gates, orders, rounds, dangling references.
Discovery
Do not readdir either root bare — both hold non-pitch entries (shapeup/knowledge-base/,
.shapeup/metrics/, .shapeup/exports/, .shapeup/pitch-archive/, loose files). A directory
counts as a pitch when:
- Committed:
shapeup/<slug>/containshill/,scopes/, orshaping/. - Local:
.shapeup/<slug>/containsreceipt.json(the mechanical "a run started" fact).
Union the two; track hasCommitted / hasLocal per slug independently. A slug can be
committed-only (shipped, local trace cleaned up), local-only (very early, before scoping), or
both.
Freshness — read this before shelling out
For every slug where hasLocal is true, freshen before reading:
node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" reduce hill --slug <slug>
node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" reduce graph --slug <slug>
For a committed-only slug (hasCommitted && !hasLocal), NEVER call reduce hill.
deriveHill() folds whatever T0 verdicts currently exist on disk; a committed-only pitch has
none (the local trace was cleaned up after shipping), so re-running it would silently regress a
true historical FINISHED down to a fabricated UPHILL_SOLVED — reading absence of evidence as
evidence of absence. Read shapeup/<slug>/hill/*.yml for those slugs exactly as committed, and
render them with the archived state the template already implements (see below). Same reasoning
applies to reduce graph — there is no local trace to append from.
Reading the hill shards
Each shapeup/<slug>/hill/<scope-id>.yml is two lines:
scope_id: <scope-id>
phase: <UPHILL_UNKNOWN|UPHILL_SOLVED|DOWNHILL_EXECUTION|FINISHED>
Parse this yourself while reading it (it is two flat string fields — no YAML library needed and
none should be added; the shipped template's browser-side JS stays dependency-free by construction).
Build one { scope_id, phase } object per file.
Rendering — the injection contract
The engine ships at assets/dashboard.template.html — a complete, self-contained HTML page
(inline CSS/JS, no external fetch beyond Google Fonts, no build step — the same convention as
this repo's own docs/visualize/*.html). Do not rewrite it from a text description; read it,
fill in real data, and write the result.
For each discovered slug, build one entry:
{ slug: "<slug>", hasLocal: <bool>, hasCommitted: <bool>, hill: [ { scope_id: "...", phase: "..." }, ... ], // parsed from the *.yml shards, [] if none graphJsonl: "<the raw text of .shapeup/<slug>/graph.jsonl, or '' when hasLocal is false>" }Collect all entries into an array,
entries.Compute
injected = JSON.stringify(JSON.stringify(entries))— stringifying twice yields a JS string literal (its own quotes and escapes included) safe to splice into the template verbatim.In the template text, replace the exact substring
"__HILL_CHART_EMBEDDED_DATA__"(quotes included) withinjected. Exactly one replacement, exactly that token.Write the result. See "Where to write it" below for the required mechanism.
The template's own boot code checks whether that constant still equals the literal placeholder (meaning nothing was injected) and falls back to an empty portfolio — the same state a project with no pitches yet would show. That fallback is what lets you smoke-test the un-injected template directly: it never throws, it just renders empty.
Where to write it
Write via Bash, not the Write or Edit tool:
cat > ".shapeup/<slug>/dashboard/hill-chart-<UTC-stamp>.html" <<'HTML'
<contents>
HTML
hooks/sandbox-guard.mjs gates Edit/Write/MultiEdit by substrate and only exempts paths
under the active order's own slug (.shapeup/<active-slug>/...); a cross-slug portfolio file
would not match that exemption if a build happens to be mid-dispatch in the same repo when you
run, and would get hook-denied. The guard does not inspect Bash, so writing the file with a
shell heredoc is unaffected by whichever order (if any) is currently active. Use:
.shapeup/<slug>/dashboard/hill-chart-<stamp>.htmlfor a single pitch's dashboard..shapeup/dashboard/portfolio-<stamp>.htmlfor the multi-pitch portfolio.
Both are LOCAL and already covered by this project's .shapeup/ gitignore rule — no new ignore
entry needed.
If the current session's tool surface includes an Artifact-publishing tool (as Claude Code /
claude.ai sessions with artifacts enabled do), also publish the same file through it, following
this repo's own artifact-design and artifact-capabilities skill guidance — the template is
already self-contained and theme-aware, so this is a strictly additive step. The local file write
above is the one guaranteed path in any host; treat the Artifact publish as a bonus, never the
only way the user gets the result.
Why the Hill Chart and the scope board are not redundant
hill.mjs's hasGreen is set by any T0-green verdict a scope has ever produced, and never
resets. A scope can therefore sit at DOWNHILL_EXECUTION on the Hill Chart while its current
attempt is red. The Hill Chart answers "how much uncertainty has been burned down, ever"; the
scope board and round matrix answer "is the current attempt green, right now." The template
renders both for exactly this reason — do not "simplify" one away later.
Invocation
# Whole portfolio, default
/hill-chart
# One pitch only
/hill-chart envlint
Standalone: discover, freshen, render, write, and (when available) publish — no flags beyond an
optional slug filter. There is no --order form; this skill is never dispatched.