Make the visible plan DOM the single source of truth: derive the spec on demand with a shared scripts/extract-plan-spec.mjs extractor, and retire the embedded #plan-digest block plus its refresh and closing-script escaping machinery — while keeping the same self-contained HTML deliverable and the ~15× token savings for downstream consumers.
Read and implement all steps in the plan at docs/plans/replace-plan-digest-with-extractor.html — Replace the stored plan digest with a compute-on-read extractor. Start from the embedded digest: awk '!f && /<script[^>]*id="plan-digest"/{f=1;next} f && /<\/script>/{exit} f' docs/plans/replace-plan-digest-with-extractor.html
Pursue as goal — optimize for the outcome
Achieve this goal: Replace the stored plan digest with a compute-on-read extractor. The plan at docs/plans/replace-plan-digest-with-extractor.html describes one approach — use it as reference, but optimize for the outcome. Start from the embedded digest: awk '!f && /<script[^>]*id="plan-digest"/{f=1;next} f && /<\/script>/{exit} f' docs/plans/replace-plan-digest-with-extractor.html
Run as workflow — launch parallel subagents
Run a workflow to implement the plan at docs/plans/replace-plan-digest-with-extractor.html — Replace the stored plan digest with a compute-on-read extractor. Brief subagents with the embedded digest: awk '!f && /<script[^>]*id="plan-digest"/{f=1;next} f && /<\/script>/{exit} f' docs/plans/replace-plan-digest-with-extractor.html
replace-plan-digest-with-extractor.html
docs/plans/replace-plan-digest-with-extractor.html
Context
Since plan-agent 2.3.0, every generated plan embeds a spec-only markdown digest — a <script type="text/markdown" id="plan-digest"> block — as the first element child of <body>. The digest is a denormalized cache of the plan spec, which already lives in the visible DOM (.objective-card, .step-card with .step-action/.step-why/.verify-body, #criteria-list, #verification).
Like any cache, that duplication creates an invalidation problem — and the codebase already pays for it: implementation-plan SKILL.md must "refresh the digest" on every content edit (Steps 2/6/8), review-plan adds a dedicated "Pass 1b — Refresh the digest", and a closing-script escaping contract guards the block. All of that machinery exists only because the spec is stored twice.
Compute-on-read removes the duplication: derive the spec from the DOM on demand. Crucially, scripts/backfill-plan-digests.mjs already exports hasDigest, decodeEntities, extractSections, and buildDigest — the exact HTML→markdown logic, already unit-tested — so this is mostly rewiring proven code, not writing a parser. New plans stop embedding the digest; the extractor reads an embedded digest first (old plans, verbatim) and derives from the DOM otherwise (new plans), so existing plans are never touched.
Files to Modify
scripts/lib/plan-spec.mjsnew shared parse fns (extractSections/buildDigest/guard+unguard)scripts/extract-plan-spec.mjsnew read-time spec extractor; imports scripts/lib/plan-spec.mjsscripts/backfill-plan-digests.mjsmodified import shared fns from scripts/lib/plan-spec.mjs (behavior unchanged).claude-plugin/marketplace.jsonmodified bump plan-agent 2.7.0 → 2.8.0- kit/plugins/plan-agent/
skills/implementation-plan/reference/SKELETON.htmlmodified remove digest block + placeholder; repoint buildImplementPrompt()skills/implementation-plan/SKILL.mdmodified drop digest section + refresh rules; prompts call extractorskills/review-plan/SKILL.mdmodified reviewers run extractor; remove refresh passskills/review-plan/references/role-prompts.mdmodified 7 reviewer briefs read via extractoragents/plan-reviewer-*.mdmodified 7 reviewer defs read via extractorREADME.mdmodified document the extractorCHANGELOG.mdmodified 2.8.0 entry- tests/plugins/
test-extract-plan-spec.mjsnew objective + unit coveragetest-extractor-wiring.shmodified renamed from test-plan-digest.sh; asserts no digest + wiringtest-goal-prompt.shmodified update any assertion on the old awk one-liner
Retained: the scripts/backfill-plan-digests.mjs injector CLI stays in place (now importing its parse fns from scripts/lib/plan-spec.mjs). Deleting the injector is deferred to Next Steps.
Steps
scripts/lib/plan-spec.mjs
unguardScriptClose helper the extractor needs. (Architecture + Conventions reviewers; pulled in per your decision.)Verify
hasDigest, decodeEntities, extractSections, buildDigest, and guardScriptClose from backfill-plan-digests.mjs into scripts/lib/plan-spec.mjs; add unguardScriptClose (reverses <\/script → </script); update backfill-plan-digests.mjs to import them from ./lib/plan-spec.mjs with no behavior change. Confirm: node tests/plugins/test-backfill-digest.mjs still passes, and node -e "import('./scripts/lib/plan-spec.mjs').then(m=>console.log(Object.keys(m)))" lists all six exports.scripts/extract-plan-spec.mjs — embedded-first, DOM-derive fallback
Verify
./lib/plan-spec.mjs; if hasDigest(html), print the embedded digest via readEmbeddedDigest(), which un-guards <\/script → </script so the output is clean markdown (the resolved contract — not the raw guarded bytes, so it will not match awk byte-for-byte); else print buildDigest(extractSections(html)). Exit non-zero with a clear message on a missing/unparseable file. Add a JSDoc header documenting the read-on-demand role and the lib dependency. Confirm: on an embedded-digest plan the output is the unguarded spec; on a digest-stripped copy it is DOM-derived with identical ## Objective/## Steps/## Verification headings; a missing file exits 1. (Resolves the Step-1 verbatim-vs-unguard contradiction — Completeness + Testability reviewers.)SKELETON.html and repoint its runtime prompt JS
<script id="plan-digest"> block and the buildImplementPrompt() JS (digestCmd at ~line 1661) that builds the Copy-button prompt at runtime.Verify
<script> block, its guiding comment, and the {plan-digest} placeholder; change buildImplementPrompt()'s digestCmd to "node scripts/extract-plan-spec.mjs " + planPath with updated instruction text. Confirm: grep -c 'id="plan-digest"' SKELETON.html returns 0, and the runtime Copy path emits the extractor — assert the buildImplementPrompt() JS constructs node scripts/extract-plan-spec.mjs (not the awk string), so clicking Copy on a rendered plan yields the new command. (Architecture + Completeness reviewers — runtime path is distinct from the static placeholder.)implementation-plan/SKILL.md — remove every digest reference
Verify
node scripts/extract-plan-spec.mjs <file>"; delete the "Machine-Readable Digest" section (~316+), the {plan-digest} fill prose (~117), the digest-refresh language in Steps 2/6/8 and the Step-8 Edit loop (~260), the "Status updates never modify the #plan-digest block" notes (~169, ~215), and the HTML-output bullet mandating the block (~296). Confirm: grep -in "machine-readable digest\|refresh the digest\|#plan-digest\|{plan-digest}" SKILL.md returns nothing, and all three prompt formats contain extract-plan-spec.mjs. (Completeness reviewer — verified orphan refs at those lines.)Verify
review-plan/SKILL.md remove the "Pass 1b — Refresh the digest" pass and update the "Lead-vs-reviewer read split" language to the extractor; in references/role-prompts.md and each of the 7 agents/plan-reviewer-*.md defs, swap the awk digest read for node scripts/extract-plan-spec.mjs with a full-HTML fallback. Confirm: grep -l extract-plan-spec.mjs agents/plan-reviewer-*.md | wc -l equals 7, the briefs file references the extractor, and no "Refresh the digest" pass remains.Verify
tests/plugins/test-extract-plan-spec.mjs: objective smoke (round-trip — write a known digest, run the extractor, assert output equals the known unguarded spec; and a digest-stripped fixture yields DOM-derived output containing ## Objective, ## Steps with ≥1 action, ## Acceptance Criteria, ## Verification, and ## Tests) plus unit cases (embedded-vs-DOM resolution; readEmbeddedDigest un-guards a <\/script fixture without stopping early; missing file exits non-zero; the scripts/lib/plan-spec.mjs import resolves). (b) git mv test-plan-digest.sh test-extractor-wiring.sh and rewrite its assertions: drop the digest-present checks, rephrase the digestCmd/SKILL/brief checks to assert the extractor, and migrate the self-quoting/guarded-close fixture into the node test. (c) Update test-backfill-digest.mjs's "every checked-in parseable plan carries a digest" assertion (~line 290) to scope to legacy/embedded plans, so committing digest-free plans does not fail it. (d) test-goal-prompt.sh needs no change (it never references the awk one-liner). Confirm all three test commands exit 0. (Testability + Completeness reviewers — verified the corpus assertion at line 290.)Verify
plan-agent/README.md to document extract-plan-spec.mjs and note that the awk one-liner returns empty for new (digest-free) plans so consumers must use the extractor; add a ## 2.8.0 CHANGELOG entry describing the compute-on-read switch, the shared scripts/lib/plan-spec.mjs, the retained backfill injector, and the awk-empty caveat; bump plan-agent in .claude-plugin/marketplace.json from 2.7.0 to 2.8.0. Confirm: grep '"version": "2.8.0"' near the plan-agent entry, a matching CHANGELOG header, and README mentions extract-plan-spec.mjs. (MINOR bump kept per your decision; awk-empty caveat per Risk reviewer.)Tests
File: tests/plugins/test-extract-plan-spec.mjs
Type: smoke test
Asserts: for a fixture plan with an embedded #plan-digest, the extractor's stdout equals the known spec markdown after un-guarding — a round-trip fixture (write a known digest, extract, compare to the unguarded expected string), not a comparison to the raw awk bytes; for a digest-stripped fixture, the stdout is DOM-derived and contains ## Objective, ## Steps (with ≥1 step action), ## Acceptance Criteria, ## Verification, and ## Tests — proving the DOM is a sufficient single source of truth.
Run: node tests/plugins/test-extract-plan-spec.mjs
readEmbeddedDigest un-guarding
File: tests/plugins/test-extract-plan-spec.mjs
Targets: the extractor's resolve logic + readEmbeddedDigest() / unguardScriptClose() helpers
Key cases: embedded digest present → unguarded embedded path; embedded absent → buildDigest(extractSections()) DOM path; a digest body containing a guarded <\/script sequence un-guards to </script> without the extractor stopping early (migrated from the old digest test's self-quoting fixture); the scripts/lib/plan-spec.mjs import resolves (guards the dependency chain); missing file exits non-zero.
File: tests/plugins/test-extractor-wiring.sh
Targets: SKELETON.html, both SKILL.md files, role-prompts.md, the 7 plan-reviewer-*.md defs, and test-backfill-digest.mjs
Key cases: skeleton contains no id="plan-digest" and its buildImplementPrompt() JS emits extract-plan-spec.mjs (not awk); implement/goal/workflow prompt formats reference the extractor; all 7 reviewer briefs and 7 agent defs reference it; no residual "Refresh the digest" pass; test-backfill-digest.mjs's corpus assertion is scoped so committed digest-free plans don't fail it.
Acceptance Criteria
Verification
Generate a fresh plan with the modified skill and confirm it has no embedded digest, its implement prompt and the Copy-button output reference the extractor, and node scripts/extract-plan-spec.mjs on it emits correct DOM-derived spec. Run the extractor on an older plan (e.g. docs/plans/embed-markdown-digest-in-html-plans.html) and confirm the output is the unguarded spec markdown (the embedded digest with <\/script reversed to </script) — not byte-identical to awk, by design. Run node tests/plugins/test-extract-plan-spec.mjs, bash tests/plugins/test-extractor-wiring.sh, and node tests/plugins/test-backfill-digest.mjs — all must pass (the last with its corpus assertion scoped to legacy plans). Finally, grep -rn "plan-digest" across the plan-agent tree and confirm only intentional references remain: the retained backfill injector, the shared scripts/lib/plan-spec.mjs, the tests, and historical CHANGELOG entries.
Completion Checklist
Completion Report
No items to report — all requirements met.
Team Review (2026-06-19 10:44:56 UTC) — 5 core reviewers
Executive Summary
Five core reviewers ran (no UI signals — the .html touched is an inert plan template). Verdict: sound direction, sound with revisions — no reviewer recommended reject. The compute-on-read approach was endorsed; findings concerned completeness and blast radius. Two findings were independently confirmed against the real files: (1) test-backfill-digest.mjs asserts every checked-in plan carries a digest and would fail once digest-free plans land; (2) the SKILL.md digest references are scattered beyond the main section.
Role-by-Role Findings
- Architecture — Sound, but the extractor importing from
backfill-plan-digests.mjsinverts the dependency direction (read tool ← injector); recommended a shared library (high-value). Flagged thebuildImplementPrompt()runtime JS (high) and thetest-backfill-digest.mjscorpus assertion (medium). Suggested phased ordering (low). - Completeness — Step 1 contradicted itself ("print exact bytes" vs "reverse the
<\/scriptguard") (high); nounguardScriptCloseexport exists (high); thetest-plan-digest.shrewrite was underspecified across its 10 assertions (high); orphan SKILL.md refs at lines 117/121/169/215 (medium); the SKELETON runtime JS (medium). - Testability — The "embedded == awk output" assertion is undefined once the embedded path un-guards (critical); the guarded-close fixture must migrate to the node test (high); the
test-backfill-digest.mjscorpus assertion will fail (high); the DOM-fallback "headings present" bar is not falsifiable without an enumerated list (medium);test-goal-prompt.shneeds no change (low). - Risk — Overall high. Semver: removing the awk contract from new plans is arguably breaking → MAJOR (high); consumers on cached older plugin versions get silent empty awk output, exit 0 (high); backfill-deletion coupling (medium); DOM selector drift causes silent degraded output for new plans (medium); old committed plans keep awk in their baked-in JS (low). Rollback is low-risk (no data mutated).
- Conventions — Good fit overall (kebab-case,
.mjswith exports, shell tests, manual semver + CHANGELOG). Nits:extract-verb deviates from the corpus/batch naming of sibling scripts (low); first cross-script import inscripts/(medium);test-extractor-wiring.shbreaks thetest-<feature>.shnoun-phrase convention — suggestedtest-extract-plan-spec.sh(low). Version bump correct.
Agreements & Conflicts
- Confirmed by ≥2 reviewers: the
buildImplementPrompt()runtime JS (Architecture + Completeness + Risk); thetest-backfill-digest.mjscorpus assertion (Testability + Architecture, verified at line ~290); the embedded verbatim-vs-unguard ambiguity (Completeness + Testability); the dependency inversion / shared-lib recommendation (Architecture + Conventions). - Conflicts with prior user decisions (surfaced, not auto-applied): semver (Risk wanted MAJOR; user kept 2.8.0 MINOR); shared-lib-now (Architecture + Conventions; user chose to pull it in — resolved); test filename (Conventions preferred
test-extract-plan-spec.sh; user kepttest-extractor-wiring.sh).
Highest-Risk Issues
- Test-corpus contradiction —
test-backfill-digest.mjswould fail on the first committed digest-free plan. → Addressed in Step 6(c). - Embedded-path contract — verbatim vs un-guarded was undefined. → Resolved: the embedded path un-guards; the objective test is a round-trip, not an awk byte-compare (Step 2 + Tests).
- Runtime Copy-button JS —
buildImplementPrompt()rebuilds the awk command at runtime. → Addressed: Step 3 repoints it and its verify asserts the Copy path emits the extractor. - Silent awk break for old consumers — cached older plugin versions get empty output on new plans. → Documented in CHANGELOG + README (Step 7); MINOR kept by decision.
- Scattered SKILL.md digest refs — beyond the main section. → Step 4 now targets every reference (lines 117/121/169/215/260/296/316+).
Triage Outcome
Accepted (applied to this plan): the test-corpus fix; the un-guarded embedded contract + round-trip objective test; broadening Steps 4 to every SKILL.md digest reference; strengthening Step 3's verify for the runtime Copy path; enumerating Step 6's test disposition + migrating the guarded-close fixture; enumerating the DOM-fallback's mandatory sections; the JSDoc coupling note; and the CHANGELOG awk-empty caveat.
Modified — pulled into scope (per your decision): the shared-library extraction was promoted from a Next Step into Step 1; the extractor and backfill both import from scripts/lib/plan-spec.mjs, and the first Next Step became "delete the now-redundant injector CLI."
Recorded, not applied: semver stays 2.8.0 MINOR (awk-empty break documented instead, per your call); the test filename stays test-extractor-wiring.sh (Conventions' test-extract-plan-spec.sh noted); Architecture's phased-ordering nit (steps already order lib/extractor before wiring); the old-committed-plan buildImplementPrompt() divergence (inherent to already-shipped plans — no migration).