Implementation Plan

Replace the stored plan digest with a compute-on-read extractor

todo
2026-06-19 agentics refactor

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.

Implement 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
File replace-plan-digest-with-extractor.html
Path docs/plans/replace-plan-digest-with-extractor.html
Acceptance criteria 0 / 8 done

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

agentics/
  • scripts/lib/plan-spec.mjs new shared parse fns (extractSections/buildDigest/guard+unguard)
  • scripts/extract-plan-spec.mjs new read-time spec extractor; imports scripts/lib/plan-spec.mjs
  • scripts/backfill-plan-digests.mjs modified import shared fns from scripts/lib/plan-spec.mjs (behavior unchanged)
  • .claude-plugin/marketplace.json modified bump plan-agent 2.7.0 → 2.8.0
  • kit/plugins/plan-agent/
    • skills/implementation-plan/reference/SKELETON.html modified remove digest block + placeholder; repoint buildImplementPrompt()
    • skills/implementation-plan/SKILL.md modified drop digest section + refresh rules; prompts call extractor
    • skills/review-plan/SKILL.md modified reviewers run extractor; remove refresh pass
    • skills/review-plan/references/role-prompts.md modified 7 reviewer briefs read via extractor
    • agents/plan-reviewer-*.md modified 7 reviewer defs read via extractor
    • README.md modified document the extractor
    • CHANGELOG.md modified 2.8.0 entry
  • tests/plugins/
    • test-extract-plan-spec.mjs new objective + unit coverage
    • test-extractor-wiring.sh modified renamed from test-plan-digest.sh; asserts no digest + wiring
    • test-goal-prompt.sh modified 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

1
todo Extract shared parse functions into scripts/lib/plan-spec.mjs
Gives the dependency graph the right shape — a shared library consumed by both the write-side backfill and the read-side extractor — instead of a read tool importing from an injector. It is also the natural home for the new unguardScriptClose helper the extractor needs. (Architecture + Conventions reviewers; pulled in per your decision.)
Verify
Move 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.
2
todo Build scripts/extract-plan-spec.mjs — embedded-first, DOM-derive fallback
One read-time tool replaces the awk one-liner across every consumer. Importing from the shared lib keeps the read path decoupled from the injector, and reusing the already-tested parse functions means no new parser.
Verify
Import from ./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.)
3
todo Strip the digest from SKELETON.html and repoint its runtime prompt JS
New plans must stop embedding the cache for the single-source-of-truth goal. The skeleton hardcodes the digest in two places: the <script id="plan-digest"> block and the buildImplementPrompt() JS (digestCmd at ~line 1661) that builds the Copy-button prompt at runtime.
Verify
Remove the digest <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.)
4
todo Rewire implementation-plan/SKILL.md — remove every digest reference
The skill instructs every consumer; it must stop generating and refreshing the digest and point all three prompt formats at the extractor. The digest is referenced in more places than the main section.
Verify
Swap the implement, goal, and workflow prompt formats to "Start from the plan spec: 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.)
5
todo Repoint the review team (SKILL + 7 briefs + 7 agent defs) to the extractor
The 5–7 reviewers were the biggest beneficiaries of the cheap read, so they must use the new path. Removing the "Pass 1b — Refresh the digest" step also deletes the last piece of sync machinery.
Verify
In 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.
6
todo Update tests — add extractor test, repurpose the digest test, fix the corpus assertion
Lock the new contract and fix the test that would otherwise fail the instant a digest-free plan is committed. The existing digest smoke test now asserts the opposite of what it used to.
Verify
(a) Add 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.)
7
todo Docs + version bump (README, CHANGELOG, marketplace.json)
Project convention requires a manual semver bump and a CHANGELOG entry on every plugin change; the README currently documents the digest + awk, and the awk-empty break for old consumers must be recorded.
Verify
Update 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

Tier 1 — Code-touching plan
Objective Extractor reproduces the spec from any plan, embedded or not

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

Unit Extractor resolution order & 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.

Integration Skeleton + skill + reviewer wiring references the extractor

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

Required

Completion Report

No items to report — all requirements met.

Next Steps

Delete the now-redundant backfill injector CLI

Paste this prompt into Claude to execute this follow-up:

The shared parse functions already live in scripts/lib/plan-spec.mjs (both the extractor and backfill-plan-digests.mjs import from it). Delete the scripts/backfill-plan-digests.mjs injector CLI — its digest-injection job is obsolete now that plans are read on demand — and migrate the still-relevant assertions from tests/plugins/test-backfill-digest.mjs (the parse/guard/unguard unit cases) onto scripts/lib/plan-spec.mjs, removing the injection-specific and corpus tests. Remove any remaining references to the injector in docs and the plan-agent README. Bump plan-agent in .claude-plugin/marketplace.json and add a CHANGELOG entry. Verify the extractor and all plugin tests still pass.
Strip embedded digests from existing docs/plans/*.html

Paste this prompt into Claude to execute this follow-up:

Run a workflow to remove the embedded <script type="text/markdown" id="plan-digest"> block (and its guiding comment) from every plan under docs/plans/, now that scripts/extract-plan-spec.mjs derives the spec from the DOM on demand. Make the edit insertion-safe and idempotent, skip docs/plans/archive, and after each removal verify node scripts/extract-plan-spec.mjs on that file still emits correct DOM-derived spec. Report the list of files changed and any that failed DOM derivation (leave those untouched).
Wish List
One canonical plan-spec API consumed everywhere Wish List

Speculative / blue-sky idea — not on the critical path. Paste into Claude when ready to explore:

Design a single canonical "plan spec" interface in scripts/lib/plan-spec.mjs that every consumer uses — the implementation-plan and review-plan skills, the plans gallery build (docs/plans/build-index.sh), the rebuild-plans-index hook, and any external tooling. The goal is that nothing parses plan HTML ad hoc: there is one extractor, one schema, and one fallback policy. Propose the module API, the migration order, and how to version the schema so older plans keep extracting. Output a decision-complete proposal in docs/proposals/.
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.mjs inverts the dependency direction (read tool ← injector); recommended a shared library (high-value). Flagged the buildImplementPrompt() runtime JS (high) and the test-backfill-digest.mjs corpus assertion (medium). Suggested phased ordering (low).
  • Completeness — Step 1 contradicted itself ("print exact bytes" vs "reverse the <\/script guard") (high); no unguardScriptClose export exists (high); the test-plan-digest.sh rewrite 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.mjs corpus assertion will fail (high); the DOM-fallback "headings present" bar is not falsifiable without an enumerated list (medium); test-goal-prompt.sh needs 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, .mjs with exports, shell tests, manual semver + CHANGELOG). Nits: extract- verb deviates from the corpus/batch naming of sibling scripts (low); first cross-script import in scripts/ (medium); test-extractor-wiring.sh breaks the test-<feature>.sh noun-phrase convention — suggested test-extract-plan-spec.sh (low). Version bump correct.

Agreements & Conflicts

  • Confirmed by ≥2 reviewers: the buildImplementPrompt() runtime JS (Architecture + Completeness + Risk); the test-backfill-digest.mjs corpus 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 kept test-extractor-wiring.sh).

Highest-Risk Issues

  1. Test-corpus contradictiontest-backfill-digest.mjs would fail on the first committed digest-free plan. → Addressed in Step 6(c).
  2. 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).
  3. Runtime Copy-button JSbuildImplementPrompt() rebuilds the awk command at runtime. → Addressed: Step 3 repoints it and its verify asserts the Copy path emits the extractor.
  4. 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.
  5. 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).