diff --git a/.changeset/fn-7149-dom-safe-html-mutation.md b/.changeset/fn-7149-dom-safe-html-mutation.md
new file mode 100644
index 0000000000..604efb3e7d
--- /dev/null
+++ b/.changeset/fn-7149-dom-safe-html-mutation.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": minor
+---
+
+summary: CE HTML docs now support DOM-validated in-place ce-doc-review fixes with report-only fallback.
+category: feature
+dev: Adds a direct parse5 CE HTML mutation helper with atomic writes, rollback, and allowlisted operations.
diff --git a/docs/workflow-editor.md b/docs/workflow-editor.md
index 71f83ac1e4..a69adb3bfe 100644
--- a/docs/workflow-editor.md
+++ b/docs/workflow-editor.md
@@ -134,7 +134,7 @@ Fusion ships built-in workflows as read-only references:
- `builtin:coding` — the default coding lifecycle and fallback for tasks without a workflow selection.
- `builtin:quick-fix` — a short path for trivial or no-commit/decision work.
- `builtin:review-heavy` — a standard execute/review/merge path with an additional gated security review.
-- `builtin:compound-engineering` — a plugin-gated Compound Engineering pipeline: `/ce-plan` writes the CE plan doc, optional `ce-doc-review` can pressure-test plans (markdown gets autofix/Open Questions write-back; HTML is report-only with no mutation), `/ce-work` implements, `/ce-code-review` gates merge, and autoMerge-off projects route through the CE PR/feedback skills before Fusion's manual merge seam.
+- `builtin:compound-engineering` — a plugin-gated Compound Engineering pipeline: `/ce-plan` writes the CE plan doc, optional `ce-doc-review` can pressure-test plans (markdown gets autofix/Open Questions write-back; HTML uses DOM-safe helper mutations only when safety is proven, otherwise report-only with no write), `/ce-work` implements, `/ce-code-review` gates merge, and autoMerge-off projects route through the CE PR/feedback skills before Fusion's manual merge seam.
- `builtin:stepwise-coding` — a graph variant that models per-step parse, execute, review, and rework structure.
- `builtin:design` — a UI-heavy work path with a gated design/UX review before standard review and merge.
diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md
index 9e3f1616db..9dc35a13b8 100644
--- a/docs/workflow-steps.md
+++ b/docs/workflow-steps.md
@@ -45,7 +45,7 @@ Decision-only or investigation tasks can also declare `noCommitsExpected` / `**N
| Quick fix | `builtin:quick-fix` | Short path for trivial or no-commit/decision work; omits the standard review stage. |
| Review-heavy | `builtin:review-heavy` | Standard execute/review/merge path with an additional gated security review. |
| Marketing | `builtin:marketing` | Content pipeline with custom Ideation, Backlog, Drafting, Editorial review, Published, and Archived columns plus structured marketing brief/draft/editorial prompts; drafts are persisted as task documents for review while the workflow reuses standard lifecycle traits and merge primitives. |
-| Compound engineering | `builtin:compound-engineering` | Plugin-gated CE workflow that invokes `/ce-plan`, optional advisory `ce-doc-review` (markdown autofix; HTML report-only), `/ce-work`, merge-blocking `/ce-code-review`, CE PR/feedback skills, Fusion merge, and learnings capture. |
+| Compound engineering | `builtin:compound-engineering` | Plugin-gated CE workflow that invokes `/ce-plan`, optional advisory `ce-doc-review` (markdown autofix; HTML DOM-safe mutation with report-only fallback), `/ce-work`, merge-blocking `/ce-code-review`, CE PR/feedback skills, Fusion merge, and learnings capture. |
| Stepwise coding | `builtin:stepwise-coding` | Graph-executor workflow that models per-step parse/execute/review/rework explicitly. |
| Design | `builtin:design` | UI-heavy work path that implements, persists a user-facing design preview task document, runs a gated design/UX review, then performs the standard review and merge. |
| PR lifecycle | `builtin:pr-workflow` | Reusable PR lifecycle graph fragment (create PR → await review → respond → gate → merge); it is a fragment, not directly selectable as a task workflow. |
@@ -140,7 +140,13 @@ The default built-in catalog entry `builtin:coding` is backed by the canonical `
`builtin:marketing` is a non-coding content workflow with marketing-specific columns (`ideation`, `backlog`, `drafting`, `editorial-review`, `published`, `archived`) and prompt seams for content brief, draft, and editorial review. Its draft stage saves the primary content deliverable as a task document for human review, while the workflow uses the same lifecycle traits (`intake`, `hold`, `wip`, `merge-blocker`, `human-review`, `complete`, `archived`) and the same merge-gate/branch-group/merge-attempt primitive region as coding workflows, so scheduler, capacity, review blocking, and merge orchestration behavior remain standard.
-`builtin:compound-engineering` is plugin-gated by `fusion-plugin-compound-engineering`. Its graph runs `/ce-plan` first and expects the CE plan document artifact under `docs/plans/`; an optional default-off `ce-doc-review` advisory step can then review plans without blocking merge. Markdown plans retain safe autofix and Append-to-Open-Questions behavior, while HTML plans run in report-only mode with no in-file mutation. Implementation runs `/ce-work`, merge-blocking code review runs `/ce-code-review`, and the PR lane runs `/ce-commit-push-pr` then `/ce-resolve-pr-feedback` before Fusion's native merge seam. When project `autoMerge` is off, that merge seam no-ops into manual review instead of forcing an unattended board merge, so the CE-created pull request remains the human merge path.
+
+
+
+`builtin:compound-engineering` is plugin-gated by `fusion-plugin-compound-engineering`. Its graph runs `/ce-plan` first and expects the CE plan document artifact under `docs/plans/`; an optional default-off `ce-doc-review` advisory step can then review plans without blocking merge. Markdown plans retain safe autofix and Append-to-Open-Questions behavior, while HTML plans support DOM-safe in-place mutation only after parse/anchor/visible-text/protected-region validation with atomic, idempotent writes; any safety failure falls back to report-only with no write. Implementation runs `/ce-work`, merge-blocking code review runs `/ce-code-review`, and the PR lane runs `/ce-commit-push-pr` then `/ce-resolve-pr-feedback` before Fusion's native merge seam. When project `autoMerge` is off, that merge seam no-ops into manual review instead of forcing an unattended board merge, so the CE-created pull request remains the human merge path.
During triage/planning sessions, agents can call `fn_workflow_list` to discover available built-in and custom workflows and read their descriptions before routing work. They can call `fn_workflow_select` only when the user explicitly requested a workflow or when selecting a workflow for a task they created, and they can pass `workflow_id` when creating child tasks with `fn_task_create`; decision-only or investigation tasks can also set `noCommitsExpected` / `**No commits expected:** true` when no code changes are expected. The built-in triage thresholds, decision-only verb list, and default routing IDs are workflow-native typed settings resolved from the selected workflow.
diff --git a/plugins/fusion-plugin-compound-engineering/package.json b/plugins/fusion-plugin-compound-engineering/package.json
index 53069732dc..d7769de7c2 100644
--- a/plugins/fusion-plugin-compound-engineering/package.json
+++ b/plugins/fusion-plugin-compound-engineering/package.json
@@ -23,6 +23,7 @@
"@fusion/core": "workspace:*",
"@fusion/plugin-sdk": "workspace:*",
"lucide-react": "^0.542.0",
+ "parse5": "^8.0.0",
"react": "^19.0.0",
"react-dom": "^19.2.4"
},
diff --git a/plugins/fusion-plugin-compound-engineering/src/__tests__/ce-doc-review-html-mode.test.ts b/plugins/fusion-plugin-compound-engineering/src/__tests__/ce-doc-review-html-mode.test.ts
index 2830674892..910ec1e1a0 100644
--- a/plugins/fusion-plugin-compound-engineering/src/__tests__/ce-doc-review-html-mode.test.ts
+++ b/plugins/fusion-plugin-compound-engineering/src/__tests__/ce-doc-review-html-mode.test.ts
@@ -31,7 +31,7 @@ function expectNoLegacyHtmlSkipLanguage(surfaces: Record): void
/*
FNXC:CompoundEngineering 2026-06-27-18:31:
-FN-7147 replaces the CE document-review HTML skip with a non-mutating report-only path. This guard reads the prompt-executed bundled skill text directly so future upstream refreshes cannot leave one handoff saying “skip HTML” while another routes HTML to review.
+FN-7147 replaces the CE document-review HTML skip with a report-only fallback path. FN-7149 permits only proven DOM-safe HTML mutations, so this guard reads the prompt-executed bundled skill text directly to prevent future upstream refreshes from reintroducing markdown mutation or HTML skip language.
*/
describe("ce-doc-review HTML report-only mode", () => {
const docReviewSkill = readSkill("ce-doc-review/SKILL.md");
@@ -39,32 +39,46 @@ describe("ce-doc-review HTML report-only mode", () => {
const planHandoff = readSkill("ce-plan/references/plan-handoff.md");
const brainstormHandoff = readSkill("ce-brainstorm/references/handoff.md");
- it("documents that HTML doc review is report-only and non-mutating", () => {
- expect(docReviewSkill).toMatch(/HTML artifacts? (?:are reviewed|receive)[^\n.]*report-only/i);
+ it("documents that HTML doc review is DOM-safe with report-only fallback", () => {
+ expect(docReviewSkill).toMatch(/HTML artifacts may receive only the DOM-safe mutations/i);
expect(docReviewSkill).toMatch(/same document-quality and persona-lens checks/i);
- expect(docReviewSkill).toMatch(/apply no fixes of any class/i);
+ expect(docReviewSkill).toMatch(/fall back to report-only \(`fixes_applied = 0`\)/i);
expect(docReviewSkill).toMatch(/Append-to-Open-Questions write-back that inserts markdown/i);
- expect(docReviewSkill).toMatch(/HTML artifacts are reviewed in report-only mode and are never mutated/i);
+ expect(docReviewSkill).toMatch(/Malformed-checklist HTML repair remains report-only/i);
+ expect(docReviewSkill).toMatch(/successful DOM-safe helper fixes may increment `fixes_applied` only for the allowlisted operations/i);
});
it("routes ce-plan HTML artifacts through report-only review instead of a skipped envelope", () => {
- expect(planHandoff).toMatch(/HTML plans use `ce-doc-review` in report-only mode/i);
- expect(planHandoff).toMatch(/applied_fixes_count = 0/i);
+ expect(planHandoff).toMatch(/HTML plans use `ce-doc-review` with DOM-safe mutation enabled only when/i);
+ expect(planHandoff).toMatch(/applied_fixes_count = 0` when the helper refuses/i);
expect(planHandoff).toMatch(/no `skipped_reason` field/i);
- expect(planHandoff).toMatch(/Free-form requests for review[^\n]*HTML plan in report-only mode/i);
+ expect(planHandoff).toMatch(/Free-form requests for review[^\n]*HTML plan in DOM-safe-or-report-only mode/i);
});
it("keeps ce-plan SKILL.md aligned with report-only HTML review", () => {
expect(planSkill).toMatch(/For HTML plans \(`OUTPUT_FORMAT=html`\)[^\n]*still runs ce-doc-review/i);
- expect(planSkill).toMatch(/HTML reviews are report-only and do not offer apply or Open Questions write-back/i);
- expect(planSkill).toMatch(/Document review is mandatory for markdown plans and report-only for HTML plans/i);
+ expect(planSkill).toMatch(/HTML reviews only apply proven DOM-safe helper fixes/i);
+ expect(planSkill).toMatch(/Document review is mandatory for markdown plans and DOM-safe-or-report-only for HTML plans/i);
});
it("shows ce-brainstorm requirements review for HTML in report-only mode", () => {
expect(brainstormHandoff).toMatch(/Shown when a unified plan artifact exists/i);
- expect(brainstormHandoff).toMatch(/Under `OUTPUT_FORMAT=html`, run `ce-doc-review` in report-only mode/i);
+ expect(brainstormHandoff).toMatch(/Under `OUTPUT_FORMAT=html`, run `ce-doc-review` in DOM-safe-or-report-only mode/i);
expect(brainstormHandoff).toMatch(/This nudge applies to markdown and HTML artifacts/i);
- expect(brainstormHandoff).toMatch(/For `\.html` artifacts, state that the review is report-only/i);
+ expect(brainstormHandoff).toMatch(/For `\.html` artifacts, state that the review is DOM-safe-or-report-only/i);
+ });
+
+
+ it("keeps HTML rendering references aligned with DOM-safe fallback", () => {
+ for (const [name, content] of Object.entries({
+ "ce-plan/references/html-rendering.md": readSkill("ce-plan/references/html-rendering.md"),
+ "ce-brainstorm/references/html-rendering.md": readSkill("ce-brainstorm/references/html-rendering.md"),
+ "ce-ideate/references/html-rendering.md": readSkill("ce-ideate/references/html-rendering.md"),
+ })) {
+ expect(content, name).toMatch(/ce-doc-review` DOM-safe-or-report-only mode/i);
+ expect(content, name).toMatch(/parse5-backed DOM-safe helper mutations/i);
+ expect(content, name).toMatch(/helper refusal falls back to report-only/i);
+ }
});
it("removes old HTML markdown-only skip language from all edited review surfaces", () => {
diff --git a/plugins/fusion-plugin-compound-engineering/src/artifacts/__tests__/html-mutation.test.ts b/plugins/fusion-plugin-compound-engineering/src/artifacts/__tests__/html-mutation.test.ts
new file mode 100644
index 0000000000..2034639df3
--- /dev/null
+++ b/plugins/fusion-plugin-compound-engineering/src/artifacts/__tests__/html-mutation.test.ts
@@ -0,0 +1,228 @@
+import { mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, describe, expect, it } from "vitest";
+
+import { applyHtmlMutations, writeHtmlMutationsToFile, type HtmlMutationOperation } from "../html-mutation.js";
+
+const BASE_HTML = 'Plan
" }], { rootDir: root });
+
+ expect(result).toMatchObject({ ok: false, fixesApplied: 0 });
+ expect(readFileSync(real, "utf8")).toBe(BASE_HTML);
+ });
+});
diff --git a/plugins/fusion-plugin-compound-engineering/src/artifacts/html-mutation.ts b/plugins/fusion-plugin-compound-engineering/src/artifacts/html-mutation.ts
new file mode 100644
index 0000000000..9fc9774fc1
--- /dev/null
+++ b/plugins/fusion-plugin-compound-engineering/src/artifacts/html-mutation.ts
@@ -0,0 +1,519 @@
+import { lstatSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
+import { basename, dirname, isAbsolute, join, relative } from "node:path";
+import { randomUUID } from "node:crypto";
+import { parse, parseFragment, serialize, serializeOuter, type DefaultTreeAdapterTypes } from "parse5";
+
+const MAX_ARTIFACT_BYTES = 2_000_000;
+const STABLE_SECTION_IDS = new Set([
+ "goal-capsule",
+ "product-contract",
+ "product-requirements",
+ "planning-contract",
+ "implementation-units",
+ "verification-contract",
+ "definition-of-done",
+ "appendix",
+ "open-questions",
+ "outstanding-questions",
+]);
+const PROTECTED_TAGS = new Set(["head", "script", "style"]);
+const RAW_TEXT_TAGS = new Set(["pre", "code", "script", "style"]);
+const SAFE_OPEN_QUESTION_TAGS = new Set([
+ "li",
+ "a",
+ "abbr",
+ "b",
+ "br",
+ "cite",
+ "code",
+ "em",
+ "i",
+ "kbd",
+ "mark",
+ "q",
+ "s",
+ "samp",
+ "small",
+ "span",
+ "strong",
+ "sub",
+ "sup",
+ "u",
+ "var",
+]);
+const SAFE_OPEN_QUESTION_GLOBAL_ATTRS = new Set(["aria-label", "title"]);
+const SAFE_OPEN_QUESTION_ATTRS = new Map>([["a", new Set(["href", "title", "aria-label"])] as const]);
+
+type Document = DefaultTreeAdapterTypes.Document;
+type DocumentFragment = DefaultTreeAdapterTypes.DocumentFragment;
+type Element = DefaultTreeAdapterTypes.Element;
+type TextNode = DefaultTreeAdapterTypes.TextNode;
+type ChildNode = DefaultTreeAdapterTypes.ChildNode;
+type ParentNode = DefaultTreeAdapterTypes.ParentNode;
+
+export type HtmlMutationOperation =
+ | { type: "append-open-question"; itemHtml: string }
+ | { type: "repair-heading-depth"; anchorId: string; fromLevel: 1 | 2 | 3 | 4 | 5 | 6; toLevel: 1 | 2 | 3 | 4 | 5 | 6 }
+ | { type: "normalize-duplicate-inter-block-whitespace" }
+ | { type: "replace-visible-text"; from: string; to: string; anchorId?: string };
+
+export interface HtmlMutationSuccess {
+ ok: true;
+ html: string;
+ fixesApplied: number;
+}
+
+export interface HtmlMutationRefusal {
+ ok: false;
+ reason: string;
+ fixesApplied: 0;
+}
+
+export type HtmlMutationResult = HtmlMutationSuccess | HtmlMutationRefusal;
+
+export interface HtmlMutationWriteSuccess {
+ ok: true;
+ fixesApplied: number;
+ path: string;
+}
+
+export type HtmlMutationWriteResult = HtmlMutationWriteSuccess | HtmlMutationRefusal;
+
+export interface HtmlMutationWriteOptions {
+ rootDir?: string;
+ validateWrittenHtml?: (html: string) => boolean;
+}
+
+interface ProtectedSnapshot {
+ protectedMarkup: string[];
+ ids: string[];
+ dataAttrs: string[];
+}
+
+/*
+FNXC:CompoundEngineering 2026-06-27-21:48:
+FN-7149 requires CE HTML fixes to use a direct parse5 parse/mutate/serialize loop, not jsdom or markdown text edits. The helper refuses unless the source is parse5 round-trip stable, anchors resolve deterministically, protected regions are byte-preserved, and the write path can roll back after post-write validation.
+*/
+
+export function applyHtmlMutations(input: string, operations: readonly HtmlMutationOperation[]): HtmlMutationResult {
+ let document = parseStableDocument(input);
+ if (!document.ok) return document;
+
+ const beforeVisibleText = getVisibleText(document.document);
+ const protectedBefore = snapshotProtectedRegions(document.document);
+ let expectedVisibleText = beforeVisibleText;
+ let fixesApplied = 0;
+
+ for (const operation of operations) {
+ const mutation = applySingleOperation(document.document, operation);
+ if (!mutation.ok) return mutation;
+ if (mutation.applied) {
+ fixesApplied += 1;
+ expectedVisibleText = mutation.expectedVisibleText ?? mutateExpectedVisibleText(expectedVisibleText, operation);
+ }
+ }
+
+ const output = serialize(document.document);
+ const reparsed = parseStableDocument(output);
+ if (!reparsed.ok) return refusal(`post-mutation validation failed: ${reparsed.reason}`);
+
+ const protectedAfter = snapshotProtectedRegions(reparsed.document);
+ if (!sameJson(protectedBefore, protectedAfter)) return refusal("protected region changed during HTML mutation");
+ if (getVisibleText(reparsed.document) !== expectedVisibleText) return refusal("visible text changed outside the intended mutation");
+
+ return { ok: true, html: output, fixesApplied };
+}
+
+export function writeHtmlMutationsToFile(
+ filePath: string,
+ operations: readonly HtmlMutationOperation[],
+ options: HtmlMutationWriteOptions = {},
+): HtmlMutationWriteResult {
+ let tempPath: string | undefined;
+ try {
+ const safePath = resolveSafeArtifactPath(filePath, options.rootDir);
+ const original = readFileSync(safePath, "utf8");
+ const result = applyHtmlMutations(original, operations);
+ if (!result.ok) return result;
+ if (result.fixesApplied === 0 || result.html === original) return { ok: true, fixesApplied: 0, path: safePath };
+
+ tempPath = join(dirname(safePath), `.${basename(safePath)}.html-mutation-${randomUUID()}.tmp`);
+ writeFileSync(tempPath, result.html, { encoding: "utf8", mode: 0o600 });
+ renameSync(tempPath, safePath);
+ tempPath = undefined;
+
+ const restoreOriginal = (): HtmlMutationRefusal => {
+ tempPath = join(dirname(safePath), `.${basename(safePath)}.html-mutation-rollback-${randomUUID()}.tmp`);
+ writeFileSync(tempPath, original, { encoding: "utf8", mode: 0o600 });
+ renameSync(tempPath, safePath);
+ tempPath = undefined;
+ return refusal("post-write validation failed; restored original HTML artifact");
+ };
+
+ /*
+ FNXC:CompoundEngineering 2026-06-28-07:56:
+ FN-7149 PR feedback requires thrown post-write validators to use the same rollback path as false validation. Headless CE mutation must never leave a validator-rejected artifact on disk after the atomic rename.
+ */
+ let postWriteFailed = false;
+ try {
+ const written = readFileSync(safePath, "utf8");
+ const postWrite = parseStableDocument(written);
+ postWriteFailed = !postWrite.ok || written !== result.html || options.validateWrittenHtml?.(written) === false;
+ } catch {
+ postWriteFailed = true;
+ }
+ if (postWriteFailed) return restoreOriginal();
+
+ return { ok: true, fixesApplied: result.fixesApplied, path: safePath };
+ } catch (error) {
+ if (tempPath) rmSync(tempPath, { force: true });
+ return refusal(error instanceof Error ? error.message : "HTML mutation write failed");
+ } finally {
+ if (tempPath) rmSync(tempPath, { force: true });
+ }
+}
+
+function parseStableDocument(input: string): { ok: true; document: Document } | HtmlMutationRefusal {
+ const document = parse(input);
+ const roundTrip = serialize(document);
+ if (roundTrip !== input) return refusal("round-trip stability gate failed");
+ return { ok: true, document };
+}
+
+function refusal(reason: string): HtmlMutationRefusal {
+ return { ok: false, reason, fixesApplied: 0 };
+}
+
+function applySingleOperation(
+ document: Document,
+ operation: HtmlMutationOperation,
+): { ok: true; applied: boolean; expectedVisibleText?: string } | HtmlMutationRefusal {
+ switch (operation.type) {
+ case "append-open-question":
+ return appendOpenQuestion(document, operation.itemHtml);
+ case "repair-heading-depth":
+ return repairHeadingDepth(document, operation);
+ case "normalize-duplicate-inter-block-whitespace":
+ return normalizeDuplicateInterBlockWhitespace(document);
+ case "replace-visible-text":
+ return replaceVisibleText(document, operation);
+ default:
+ return refusal("unsupported HTML mutation operation");
+ }
+}
+
+/**
+ * FNXC:CompoundEngineering 2026-06-27-21:49:
+ * Append-to-Open-Questions is portable to HTML only as a parsed single `
` fragment under an existing Open/Outstanding Questions list. The helper must not fabricate sections or inject script/style-capable fragments because FN-7147's report-only fallback is safer than guessing the CE renderer's structure.
+ */
+function appendOpenQuestion(document: Document, itemHtml: string): { ok: true; applied: boolean } | HtmlMutationRefusal {
+ const anchor = resolveOpenQuestionsAnchor(document);
+ if (!anchor.ok) return anchor;
+ const list = resolveQuestionList(anchor.element);
+ if (!list.ok) return list;
+ const item = parseListItemFragment(itemHtml);
+ if (!item.ok) return item;
+ const itemMarkup = serializeOuter(item.element);
+ if (list.element.childNodes.some((child) => isElement(child) && child.tagName === "li" && serializeOuter(child) === itemMarkup)) {
+ return { ok: true, applied: false };
+ }
+ item.element.parentNode = list.element;
+ list.element.childNodes.push(item.element);
+ return { ok: true, applied: true };
+}
+
+function repairHeadingDepth(
+ document: Document,
+ operation: Extract,
+): { ok: true; applied: boolean } | HtmlMutationRefusal {
+ if (!STABLE_SECTION_IDS.has(operation.anchorId)) return refusal("heading repair anchor is not in the stable CE section registry");
+ const anchor = resolveUniqueElementById(document, operation.anchorId);
+ if (!anchor.ok) return anchor;
+ if (!isHeading(anchor.element)) return refusal("heading repair anchor does not resolve to a heading element");
+ if (anchor.element.tagName === `h${operation.toLevel}`) return { ok: true, applied: false };
+ if (anchor.element.tagName !== `h${operation.fromLevel}`) return refusal("heading repair source level does not match the anchored element");
+ anchor.element.nodeName = `h${operation.toLevel}`;
+ anchor.element.tagName = `h${operation.toLevel}`;
+ return { ok: true, applied: true };
+}
+
+/**
+ * FNXC:CompoundEngineering 2026-06-27-21:50:
+ * Duplicate whitespace normalization is limited to adjacent inter-block text nodes outside raw-text elements. It never rewrites text inside prose, pre/code, script, or style, so rendered words and executable/style content stay byte-identical.
+ */
+function normalizeDuplicateInterBlockWhitespace(document: Document): { ok: true; applied: boolean } | HtmlMutationRefusal {
+ let applied = false;
+ walkParents(document, (parent) => {
+ if (isElement(parent) && RAW_TEXT_TAGS.has(parent.tagName)) return;
+ for (let index = parent.childNodes.length - 1; index > 0; index -= 1) {
+ const current = parent.childNodes[index];
+ const previous = parent.childNodes[index - 1];
+ if (isWhitespaceText(current) && isWhitespaceText(previous)) {
+ parent.childNodes.splice(index, 1);
+ applied = true;
+ }
+ }
+ for (let index = 1; index < parent.childNodes.length - 1; index += 1) {
+ const current = parent.childNodes[index];
+ if (!isWhitespaceText(current) || !/\n\s*\n/.test(current.value)) continue;
+ const previous = parent.childNodes[index - 1];
+ const next = parent.childNodes[index + 1];
+ if (isElement(previous) && isElement(next)) {
+ current.value = "\n";
+ applied = true;
+ }
+ }
+ });
+ return { ok: true, applied };
+}
+
+/**
+ * FNXC:CompoundEngineering 2026-06-27-21:51:
+ * Typo repair is constrained to one exact visible text-node substring. Ambiguous matches, protected-region matches, and cross-node wording edits stay report-only because they cannot prove visible-prose equivalence without human judgment.
+ *
+ * FNXC:CompoundEngineering 2026-06-28-07:49:
+ * FN-7149 PR feedback found that expected visible text must reconcile against the selected text node's occurrence, not the first occurrence in the concatenated document. Anchor-scoped replacements can target a later node while earlier prose still contains the same word.
+ */
+function replaceVisibleText(
+ document: Document,
+ operation: Extract,
+): { ok: true; applied: boolean; expectedVisibleText?: string } | HtmlMutationRefusal {
+ if (!operation.from || operation.from === operation.to) return { ok: true, applied: false };
+ const root = operation.anchorId ? resolveUniqueElementById(document, operation.anchorId) : { ok: true as const, element: document };
+ if (!root.ok) return root;
+ const matches: TextNode[] = [];
+ walkNodes(root.element, (node, ancestors) => {
+ if (!isText(node) || isInsideProtectedOrRawText(ancestors)) return;
+ if (countOccurrences(node.value, operation.from) === 1) matches.push(node);
+ });
+ if (matches.length !== 1) return refusal("visible text replacement did not resolve to exactly one text node");
+ const expectedVisibleText = replaceVisibleTextAtNode(document, matches[0], operation.from, operation.to);
+ matches[0].value = matches[0].value.replace(operation.from, operation.to);
+ return { ok: true, applied: true, expectedVisibleText };
+}
+
+function replaceVisibleTextAtNode(document: Document, target: TextNode, from: string, to: string): string {
+ let rawVisibleText = "";
+ walkNodes(document, (node, ancestors) => {
+ if (!isText(node) || isInsideProtectedOrRawText(ancestors)) return;
+ rawVisibleText += node === target ? node.value.replace(from, to) : node.value;
+ });
+ return normalizeVisibleText(rawVisibleText);
+}
+
+function mutateExpectedVisibleText(text: string, operation: HtmlMutationOperation): string {
+ if (operation.type === "append-open-question") return normalizeVisibleText(`${text}${getVisibleText(parseFragment(operation.itemHtml))}`);
+ if (operation.type === "replace-visible-text") return normalizeVisibleText(text.replace(operation.from, operation.to));
+ return text;
+}
+
+function resolveOpenQuestionsAnchor(document: Document): { ok: true; element: Element } | HtmlMutationRefusal {
+ const byId = uniqueElements(
+ ["open-questions", "outstanding-questions"].flatMap((id) => findElements(document, (el) => getAttr(el, "id") === id)),
+ );
+ if (byId.length === 1) return { ok: true, element: byId[0] };
+ if (byId.length > 1) return refusal("Open Questions anchor is ambiguous");
+
+ const byHeading = uniqueElements(
+ findElements(document, (el) => isHeading(el) && /^(open|outstanding) questions$/i.test(getVisibleText(el).trim())),
+ );
+ if (byHeading.length !== 1) return refusal(byHeading.length === 0 ? "Open Questions anchor not found" : "Open Questions anchor is ambiguous");
+ return { ok: true, element: byHeading[0] };
+}
+
+function resolveQuestionList(anchor: Element): { ok: true; element: Element } | HtmlMutationRefusal {
+ const candidates: Element[] = [];
+ if (anchor.tagName === "section" || anchor.tagName === "article" || anchor.tagName === "div") {
+ candidates.push(...findElements(anchor, (el) => el !== anchor && (el.tagName === "ul" || el.tagName === "ol")));
+ } else if (isHeading(anchor) && anchor.parentNode && "childNodes" in anchor.parentNode) {
+ const siblings = anchor.parentNode.childNodes;
+ const start = siblings.indexOf(anchor);
+ const anchorLevel = headingLevel(anchor);
+ for (const sibling of siblings.slice(start + 1)) {
+ if (isElement(sibling) && isHeading(sibling) && headingLevel(sibling) <= anchorLevel) break;
+ if (isElement(sibling) && (sibling.tagName === "ul" || sibling.tagName === "ol")) candidates.push(sibling);
+ if (isElement(sibling)) candidates.push(...findElements(sibling, (el) => el.tagName === "ul" || el.tagName === "ol"));
+ }
+ }
+ const unique = uniqueElements(candidates);
+ if (unique.length !== 1) return refusal(unique.length === 0 ? "Open Questions list not found" : "Open Questions list is ambiguous");
+ return { ok: true, element: unique[0] };
+}
+
+function parseListItemFragment(itemHtml: string): { ok: true; element: Element } | HtmlMutationRefusal {
+ const fragment = parseFragment(itemHtml);
+ const elementChildren = fragment.childNodes.filter(isElement);
+ if (elementChildren.length !== 1 || fragment.childNodes.some((node) => !isWhitespaceText(node) && !isElement(node))) {
+ return refusal("Open Questions append requires exactly one list item fragment");
+ }
+ const [item] = elementChildren;
+ if (item.tagName !== "li") return refusal("Open Questions append fragment must be a list item");
+ const safety = validateOpenQuestionListItem(item);
+ if (!safety.ok) return safety;
+ return { ok: true, element: item };
+}
+
+/*
+FNXC:CompoundEngineering 2026-06-28-08:02:
+FN-7149 PR feedback requires Open Questions HTML append to be text-with-minimal-inline-markup, not an open HTML passthrough. A strict tag/attribute allowlist rejects active elements, handlers, srcdoc, and active URL schemes before generated CE documents can persist model-shaped HTML.
+*/
+function validateOpenQuestionListItem(item: Element): { ok: true } | HtmlMutationRefusal {
+ for (const element of findElements(item, () => true)) {
+ if (!SAFE_OPEN_QUESTION_TAGS.has(element.tagName)) {
+ return refusal("Open Questions append fragment contains an unsafe element");
+ }
+ for (const attr of element.attrs) {
+ const attrName = attr.name.toLowerCase();
+ if (attrName.startsWith("on") || attrName === "srcdoc") {
+ return refusal("Open Questions append fragment contains an unsafe attribute");
+ }
+ const tagAttrs = SAFE_OPEN_QUESTION_ATTRS.get(element.tagName);
+ if (!SAFE_OPEN_QUESTION_GLOBAL_ATTRS.has(attrName) && !tagAttrs?.has(attrName)) {
+ return refusal("Open Questions append fragment contains an unsupported attribute");
+ }
+ if (attrName === "href" && !isSafeOpenQuestionHref(attr.value)) {
+ return refusal("Open Questions append fragment contains an unsafe URL");
+ }
+ }
+ }
+ return { ok: true };
+}
+
+function isSafeOpenQuestionHref(value: string): boolean {
+ const trimmed = [...value.trim()]
+ .filter((char) => {
+ const code = char.charCodeAt(0);
+ return code > 0x1f && code !== 0x7f && !/\s/.test(char);
+ })
+ .join("")
+ .toLowerCase();
+ return (
+ trimmed.startsWith("#") ||
+ trimmed.startsWith("/") ||
+ trimmed.startsWith("./") ||
+ trimmed.startsWith("../") ||
+ trimmed.startsWith("http://") ||
+ trimmed.startsWith("https://") ||
+ trimmed.startsWith("mailto:") ||
+ !/^[a-z][a-z0-9+.-]*:/i.test(trimmed)
+ );
+}
+
+function resolveUniqueElementById(document: Document, id: string): { ok: true; element: Element } | HtmlMutationRefusal {
+ const matches = findElements(document, (el) => getAttr(el, "id") === id);
+ if (matches.length !== 1) return refusal(matches.length === 0 ? `anchor id not found: ${id}` : `anchor id is ambiguous: ${id}`);
+ return { ok: true, element: matches[0] };
+}
+
+function resolveSafeArtifactPath(filePath: string, rootDir?: string): string {
+ const stat = lstatSync(filePath);
+ if (stat.isSymbolicLink()) throw new Error("Symlink HTML artifacts are not allowed");
+ if (!stat.isFile()) throw new Error("HTML mutation target must be a file");
+ if (stat.size > MAX_ARTIFACT_BYTES) throw new Error("HTML artifact exceeds mutation size limit");
+ const realFile = realpathSync(filePath);
+ if (rootDir) {
+ const realRoot = realpathSync(rootDir);
+ const rel = relative(realRoot, realFile);
+ if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("HTML mutation target escapes the project root");
+ }
+ return realFile;
+}
+
+function snapshotProtectedRegions(root: ParentNode): ProtectedSnapshot {
+ const protectedMarkup: string[] = [];
+ const ids: string[] = [];
+ const dataAttrs: string[] = [];
+ walkNodes(root, (node) => {
+ if (!isElement(node)) return;
+ if (PROTECTED_TAGS.has(node.tagName)) protectedMarkup.push(serializeOuter(node));
+ const id = getAttr(node, "id");
+ if (id) ids.push(id);
+ for (const attr of node.attrs) {
+ if (attr.name.startsWith("data-")) dataAttrs.push(`${attr.name}=${attr.value}`);
+ }
+ });
+ return { protectedMarkup, ids, dataAttrs };
+}
+
+function getVisibleText(root: ParentNode): string {
+ let text = "";
+ walkNodes(root, (node, ancestors) => {
+ if (isText(node) && !isInsideProtectedOrRawText(ancestors)) text += node.value;
+ });
+ return normalizeVisibleText(text);
+}
+
+function normalizeVisibleText(text: string): string {
+ return text.replace(/\s+/g, " ").trim();
+}
+
+function isInsideProtectedOrRawText(ancestors: readonly ParentNode[]): boolean {
+ return ancestors.some((ancestor) => isElement(ancestor) && (PROTECTED_TAGS.has(ancestor.tagName) || RAW_TEXT_TAGS.has(ancestor.tagName)));
+}
+
+function walkParents(node: ParentNode, visit: (node: ParentNode) => void): void {
+ visit(node);
+ for (const child of node.childNodes) {
+ if (isParent(child)) walkParents(child, visit);
+ }
+}
+
+function walkNodes(node: ParentNode | ChildNode, visit: (node: ParentNode | ChildNode, ancestors: ParentNode[]) => void, ancestors: ParentNode[] = []): void {
+ visit(node, ancestors);
+ if (!isParentLike(node)) return;
+ for (const child of node.childNodes) {
+ walkNodes(child, visit, [...ancestors, node]);
+ }
+}
+
+function findElements(root: ParentNode, predicate: (element: Element) => boolean): Element[] {
+ const matches: Element[] = [];
+ walkNodes(root, (node) => {
+ if (isElement(node) && predicate(node)) matches.push(node);
+ });
+ return matches;
+}
+
+function uniqueElements(elements: Element[]): Element[] {
+ return [...new Set(elements)];
+}
+
+function isParent(node: ChildNode): node is Element {
+ return "childNodes" in node;
+}
+
+function isParentLike(node: ParentNode | ChildNode): node is ParentNode {
+ return "childNodes" in node;
+}
+
+function isElement(node: ParentNode | ChildNode): node is Element {
+ return "tagName" in node;
+}
+
+function isText(node: ParentNode | ChildNode): node is TextNode {
+ return node.nodeName === "#text";
+}
+
+function isWhitespaceText(node: ChildNode): node is TextNode {
+ return isText(node) && /^\s*$/.test(node.value);
+}
+
+function isHeading(element: Element): boolean {
+ return /^h[1-6]$/.test(element.tagName);
+}
+
+function headingLevel(element: Element): number {
+ return Number(element.tagName.slice(1));
+}
+
+function getAttr(element: Element, name: string): string | undefined {
+ return element.attrs.find((attr) => attr.name === name)?.value;
+}
+
+function countOccurrences(text: string, needle: string): number {
+ return text.split(needle).length - 1;
+}
+
+function sameJson(a: unknown, b: unknown): boolean {
+ return JSON.stringify(a) === JSON.stringify(b);
+}
diff --git a/plugins/fusion-plugin-compound-engineering/src/skills/ce-brainstorm/references/handoff.md b/plugins/fusion-plugin-compound-engineering/src/skills/ce-brainstorm/references/handoff.md
index 1d12e51b09..a484bee813 100644
--- a/plugins/fusion-plugin-compound-engineering/src/skills/ce-brainstorm/references/handoff.md
+++ b/plugins/fusion-plugin-compound-engineering/src/skills/ce-brainstorm/references/handoff.md
@@ -55,9 +55,9 @@ Present only the options that apply. Renumber so visible options stay contiguous
2. **Ship it autonomously with `lfg`** - Hand the requirements to the full autonomous pipeline: `lfg` plans (`ce-plan`), implements, simplifies, runs independent code review and applies the fixes, opens a PR, and watches CI to green — hands-off, no check-ins. It plans first (unlike a raw `/goal` straight from requirements), so it's the safer autonomous path. Best when you trust the requirements and want it built and shipped without steering. **Opens a PR and pushes a branch.** Shown only for software brainstorms (`execution: code`) with `Resolve Before Planning` empty **and a unified plan artifact was created** — `lfg` hands `ce-plan` that artifact path in pipeline mode and cannot prompt, so with no artifact (e.g. a brief-alignment brainstorm that skipped doc creation per the "Decide whether a doc is warranted" rule) there is nothing to enrich; offer option 1 instead, which can plan interactively from the conversation. For a quicker plan-then-decide flow, or to run a `/goal` yourself, pick option 1 and choose at the `ce-plan` handoff.
-3. **Pressure-test the requirements** - Dispatch reviewer agents with `ce-doc-review` to find gaps, conflicts, weak premises, and scope issues in the requirements; auto-apply safe fixes for markdown; route the rest interactively. Shown when a unified plan artifact exists. Under `OUTPUT_FORMAT=html`, run `ce-doc-review` in report-only mode: persona lenses run and findings are presented, but no in-file mutations, markdown apply-set edits, or Append-to-Open-Questions write-back are offered.
+3. **Pressure-test the requirements** - Dispatch reviewer agents with `ce-doc-review` to find gaps, conflicts, weak premises, and scope issues in the requirements; auto-apply safe fixes for markdown; route the rest interactively. Shown when a unified plan artifact exists. Under `OUTPUT_FORMAT=html`, run `ce-doc-review` in DOM-safe-or-report-only mode: persona lenses run and findings are presented, only proven DOM-safe helper mutations may apply, and no markdown apply-set edits or markdown Append-to-Open-Questions write-back are offered.
4. **Publish to Proof — shareable link** - Publish the markdown unified plan to Every's Proof editor and get a shareable link to read, comment on, or share with others. One-way: the local doc stays canonical. Shown only when a markdown unified plan exists. **Render only when `OUTPUT_FORMAT=md`** (Proof operates on markdown and cannot ingest HTML).
4. **Open in browser** — open the HTML unified plan locally for review and sharing. Shown only when an HTML unified plan exists. **Render only when `OUTPUT_FORMAT=html`.** Replaces "Publish to Proof" at the same slot under exclusive output mode — the artifact is either markdown OR HTML, never both, so exactly one of the two labels applies per run.
5. **More clarifying questions to sharpen the doc** - Keep refining scope, edge cases, constraints, and preferences through further dialogue. Always shown.
@@ -83,11 +83,11 @@ re-scanning the repo. Do not print the closing summary first.
**If user selects "Pressure-test the requirements":**
Load the `ce-doc-review` skill, passing the unified plan path as the argument.
-For `.html` artifacts, state that the review is report-only and that no
-autofix or Append-to-Open-Questions write-back will run. When ce-doc-review
+For `.html` artifacts, state that the review is DOM-safe-or-report-only and that no
+markdown autofix or markdown Append-to-Open-Questions write-back will run. When ce-doc-review
returns "Review complete", return to the Phase 4 options and re-render the
-menu (the requirements may have changed for markdown; HTML findings are
-advisory unless the user chooses to edit the artifact manually, so still
+menu (the requirements may have changed for markdown or from proven DOM-safe HTML fixes; HTML findings are
+advisory when the helper refuses, so still
re-evaluate `Resolve Before Planning`, the lfg software gate, and residual
findings). If residual P0/P1 findings remain unaddressed, include the
post-review nudge above the menu. Do not show the closing summary yet.
diff --git a/plugins/fusion-plugin-compound-engineering/src/skills/ce-brainstorm/references/html-rendering.md b/plugins/fusion-plugin-compound-engineering/src/skills/ce-brainstorm/references/html-rendering.md
index 15f049302f..6cab04a2fb 100644
--- a/plugins/fusion-plugin-compound-engineering/src/skills/ce-brainstorm/references/html-rendering.md
+++ b/plugins/fusion-plugin-compound-engineering/src/skills/ce-brainstorm/references/html-rendering.md
@@ -10,10 +10,10 @@ content rendered by different skills shares the same HTML principles.
The HTML artifact is the *only* artifact the skill produces for that run —
output mode is exclusive (markdown OR HTML, never both). Downstream
-consumers that read HTML today (`ce-work`, `ce-doc-review` report-only mode,
+consumers that read HTML today (`ce-work`, `ce-doc-review` DOM-safe-or-report-only mode,
human readers) do so directly; the agent-consumability rules below make that
-work. `ce-doc-review` reviews HTML without mutation: markdown autofix and
-Append-to-Open-Questions write-back remain disabled for `.html` artifacts.
+work. `ce-doc-review` may apply only parse5-backed DOM-safe helper mutations; markdown autofix and
+markdown Append-to-Open-Questions write-back remain disabled for `.html` artifacts, and any helper refusal falls back to report-only.
## Hard invariants
@@ -543,11 +543,11 @@ fine when the content suggests them.
## Agent-consumability rules
Downstream agents that read HTML today (`ce-work`, `ce-doc-review` in
-report-only mode, a skill re-reading its own prior artifact on a resume run,
+DOM-safe-or-report-only mode, a skill re-reading its own prior artifact on a resume run,
future consumers) reason over the HTML as text — the way they reason over
markdown, not via DOM extraction or a script-style parse. `ce-doc-review`
-uses that text-readable structure for persona findings but does not mutate
-HTML (see opening note).
+uses that text-readable structure for persona findings and may mutate only through the FN-7149 DOM-safe helper
+(see opening note).
These rules are why such a consumer can locate one item (a single
requirement, unit, idea, or other ID-bearing entry) and reason over it from
diff --git a/plugins/fusion-plugin-compound-engineering/src/skills/ce-doc-review/SKILL.md b/plugins/fusion-plugin-compound-engineering/src/skills/ce-doc-review/SKILL.md
index fe86bc665e..27bbe56c85 100644
--- a/plugins/fusion-plugin-compound-engineering/src/skills/ce-doc-review/SKILL.md
+++ b/plugins/fusion-plugin-compound-engineering/src/skills/ce-doc-review/SKILL.md
@@ -1,22 +1,22 @@
---
name: ce-doc-review
-description: "Review Compound Engineering markdown or HTML plan documents for coherence, feasibility, and scope alignment. Use headless mode for automated plan handoff review; HTML is report-only."
+description: "Review Compound Engineering markdown or HTML plan documents for coherence, feasibility, and scope alignment. Use headless mode for automated plan handoff review; HTML uses DOM-safe mutation only when proven safe, otherwise report-only."
argument-hint: "[mode:headless] "
---
# CE Document Review
-Review a Compound Engineering markdown or HTML plan/requirements document. Markdown artifacts may receive safe markdown-only fixes; HTML artifacts are reviewed in report-only mode and are never mutated.
+Review a Compound Engineering markdown or HTML plan/requirements document. Markdown artifacts may receive safe markdown-only fixes; HTML artifacts may receive only the DOM-safe mutations proven by the FN-7149 helper and otherwise remain report-only with no mutation.
## Modes
-- `mode:headless `: run an automated advisory pass and return a concise review envelope. For markdown, apply only `safe_auto` markdown fixes; for HTML, report findings only and apply nothing.
-- `` without `mode:headless`: run the same review for an interactive caller. For markdown, include actionable findings clearly enough for the caller to decide what to apply; for HTML, present a report-only summary and do not offer apply or Append-to-Open-Questions write-back options.
+- `mode:headless `: run an automated advisory pass and return a concise review envelope. For markdown, apply only `safe_auto` markdown fixes; for HTML, attempt only the allowlisted DOM-safe helper operations and fall back to report-only (`fixes_applied = 0`) whenever safety cannot be proven.
+- `` without `mode:headless`: run the same review for an interactive caller. For markdown, include actionable findings clearly enough for the caller to decide what to apply; for HTML, present findings with DOM-safe mutation status and do not offer markdown apply-set or markdown Append-to-Open-Questions write-back options.
## Review boundary
@@ -31,16 +31,16 @@ FN-7147 requires HTML artifacts to receive the same persona-lens document review
1. Resolve the target path from the arguments. Prefer an explicit path. If no path is provided, inspect `docs/plans/` for the most recent markdown (`.md`) or HTML (`.html`) plan-like artifact and use that; if none exists, skip non-blockingly.
2. Classify the target type:
- Markdown (`.md`): review with markdown-safe mutation enabled for `safe_auto` fixes only.
- - HTML (`.html`): review in report-only mode. Run the same document-quality and persona-lens checks, preserve classifications, return structured findings text, and set `fixes_applied`/`applied_fixes_count` to `0`. Do not run `safe_auto` writes, `gated_auto`/`manual` apply-set edits, or Append-to-Open-Questions write-back that inserts markdown `##`/`###` headings.
+ - HTML (`.html`): review with DOM-safe mutation enabled only for the FN-7149 allowlist: append to an existing Open/Outstanding Questions list, provable stable-registry heading-depth repair, duplicate inter-block whitespace normalization, and exact visible-prose typo text-node fixes. Run the same document-quality and persona-lens checks, preserve classifications, and fall back to report-only with `fixes_applied`/`applied_fixes_count` set to `0` whenever the helper refuses. Do not run markdown `safe_auto` writes, `gated_auto`/`manual` apply-set edits, or Append-to-Open-Questions write-back that inserts markdown `##`/`###` headings.
- Any other type: skip non-blockingly and explain that only markdown and HTML CE plan/requirements artifacts are supported.
-3. Read the document enough to evaluate structure and consistency. For long documents, scan headings first, then read the Goal Capsule/Product Contract/Plan/Implementation Units/Verification/Definition of Done sections as present. For HTML, use the rendered document text/semantic headings as review input; do not rewrite tags or inject markdown.
+3. Read the document enough to evaluate structure and consistency. For long documents, scan headings first, then read the Goal Capsule/Product Contract/Plan/Implementation Units/Verification/Definition of Done sections as present. For HTML, use the rendered document text/semantic headings as review input; never rewrite tags with markdown text edits, inject markdown, or mutate outside the DOM-safe helper.
4. Check for:
- Product scope drift: requirements or Product Contract rewritten without a clear preservation note.
- HOW gaps: implementation units lacking files, dependencies, risks, or verification scenarios.
- Coherence gaps: contradictory decisions, stale handoff instructions, duplicated or inconsistent artifact readiness metadata.
- Feasibility gaps: sequencing that cannot work, missing prerequisite decisions, or verification that cannot prove the stated Definition of Done.
- - Markdown-only hygiene that is safe to fix automatically: broken heading levels, obvious duplicate blank lines, malformed checklists, or typo-level wording that does not change meaning. This check may produce report-only findings for HTML but must not edit HTML.
-5. In headless mode, apply only `safe_auto` fixes directly to markdown files. Do not apply changes that alter product scope, technical decisions, acceptance criteria, or verification obligations; report those as findings. In HTML mode, apply no fixes of any class and return the review envelope with counts/classifications intact.
+ - Hygiene that is safe to fix automatically in the target format: markdown-only broken heading levels, duplicate blank lines, malformed checklists, or typo-level wording for markdown; for HTML, only the four DOM-safe helper operations may apply. Malformed-checklist HTML repair remains report-only until CE defines a canonical HTML checklist representation.
+5. In headless mode, apply only `safe_auto` fixes directly to markdown files. Do not apply changes that alter product scope, technical decisions, acceptance criteria, or verification obligations; report those as findings. In HTML mode, call the DOM-safe helper only for allowlisted operations; on any round-trip, anchor, protected-region, visible-text, validation, unsupported-operation, or write failure, apply nothing and return the review envelope with `fixes_applied = 0` and classifications intact.
6. If findings remain, classify each as:
- `proposed_fix`: a safe but non-trivial improvement the user may accept.
- `decision`: a scope/technical judgment that needs human or planner choice.
@@ -62,6 +62,6 @@ Use this shape for the final line:
- `APPROVE_WITH_NOTES`: non-blocking observations or report-only HTML findings; this is the normal result for optional workflow use.
- `REVISE`: only for severe document issues that make downstream work unsafe or impossible. In the Fusion built-in workflow this skill is advisory/non-blocking, but the verdict still helps humans see severity.
-For HTML report-only reviews, the JSON must still be emitted, `fixes_applied` must be `0`, and notes should mention that HTML was reviewed without autofix.
+For HTML reviews, the JSON must still be emitted. When DOM-safe mutation is refused or unsupported, `fixes_applied` must be `0`, and notes should mention that HTML fell back to report-only without autofix; successful DOM-safe helper fixes may increment `fixes_applied` only for the allowlisted operations.
Do not wrap the final JSON in markdown fences.
diff --git a/plugins/fusion-plugin-compound-engineering/src/skills/ce-ideate/references/html-rendering.md b/plugins/fusion-plugin-compound-engineering/src/skills/ce-ideate/references/html-rendering.md
index 15f049302f..6cab04a2fb 100644
--- a/plugins/fusion-plugin-compound-engineering/src/skills/ce-ideate/references/html-rendering.md
+++ b/plugins/fusion-plugin-compound-engineering/src/skills/ce-ideate/references/html-rendering.md
@@ -10,10 +10,10 @@ content rendered by different skills shares the same HTML principles.
The HTML artifact is the *only* artifact the skill produces for that run —
output mode is exclusive (markdown OR HTML, never both). Downstream
-consumers that read HTML today (`ce-work`, `ce-doc-review` report-only mode,
+consumers that read HTML today (`ce-work`, `ce-doc-review` DOM-safe-or-report-only mode,
human readers) do so directly; the agent-consumability rules below make that
-work. `ce-doc-review` reviews HTML without mutation: markdown autofix and
-Append-to-Open-Questions write-back remain disabled for `.html` artifacts.
+work. `ce-doc-review` may apply only parse5-backed DOM-safe helper mutations; markdown autofix and
+markdown Append-to-Open-Questions write-back remain disabled for `.html` artifacts, and any helper refusal falls back to report-only.
## Hard invariants
@@ -543,11 +543,11 @@ fine when the content suggests them.
## Agent-consumability rules
Downstream agents that read HTML today (`ce-work`, `ce-doc-review` in
-report-only mode, a skill re-reading its own prior artifact on a resume run,
+DOM-safe-or-report-only mode, a skill re-reading its own prior artifact on a resume run,
future consumers) reason over the HTML as text — the way they reason over
markdown, not via DOM extraction or a script-style parse. `ce-doc-review`
-uses that text-readable structure for persona findings but does not mutate
-HTML (see opening note).
+uses that text-readable structure for persona findings and may mutate only through the FN-7149 DOM-safe helper
+(see opening note).
These rules are why such a consumer can locate one item (a single
requirement, unit, idea, or other ID-bearing entry) and reason over it from
diff --git a/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/SKILL.md b/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/SKILL.md
index 2d80f23eb3..d970d06352 100644
--- a/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/SKILL.md
+++ b/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/SKILL.md
@@ -736,11 +736,11 @@ Then continue to Phase 5.2 without a blocking question.
**REQUIRED: Write the plan file to disk before presenting any options.**
-HTML note: `ce-doc-review` runs for HTML plans in report-only mode. HTML plans still render the unified artifact; the Phase 5.3.8 document-review pass reviews the `.html` plan without autofix or Append-to-Open-Questions write-back.
+HTML note: `ce-doc-review` runs for HTML plans in DOM-safe-or-report-only mode. HTML plans still render the unified artifact; the Phase 5.3.8 document-review pass may apply only proven DOM-safe helper fixes and otherwise reviews the `.html` plan without markdown autofix or markdown Append-to-Open-Questions write-back.
Use the Write tool to save the complete plan to the resolved format's extension:
@@ -764,7 +764,7 @@ Write the unified plan artifact according to `references/plan-sections.md`.
- Do not set `artifact_contract: ce-unified-plan/v1` on universal-planning outputs, answer-seeking outputs, or approach-plans unless they include the full software implementation contract.
- Do not write a launch prompt into the doc. The launch prompt is generated at handoff (Phase 5.4 menu — `/goal` copy-paste on Claude Code, `create_goal` on Codex) from the plan's current content, so it never goes stale; it points to Goal Capsule, Verification Contract, Definition of Done, and U-IDs rather than duplicating them.
-**HTML composition timing.** When `OUTPUT_FORMAT=html`, Phase 5.3 deepening runs before this write completes its final form, and `ce-doc-review` then reviews the HTML artifact in report-only mode (see Phase 5.3.8 format gate in `references/plan-handoff.md`). The HTML artifact reflects deepening synthesis and receives doc-review findings, but no in-file autofix or Append-to-Open-Questions mutation is attempted.
+**HTML composition timing.** When `OUTPUT_FORMAT=html`, Phase 5.3 deepening runs before this write completes its final form, and `ce-doc-review` then reviews the HTML artifact in DOM-safe-or-report-only mode (see Phase 5.3.8 format gate in `references/plan-handoff.md`). The HTML artifact reflects deepening synthesis and receives doc-review findings; only proven DOM-safe helper fixes may mutate it, and markdown autofix or markdown Append-to-Open-Questions mutation is never attempted.
Confirm (use absolute path so the reference is clickable in modern terminals):
@@ -817,7 +817,7 @@ Build a risk profile. Treat these as high-risk signals:
- **Thin local grounding override:** If Phase 1.2 triggered external research because local patterns were thin (fewer than 3 direct examples or adjacent-domain match), always proceed to scoring regardless of how grounded the plan appears. When the plan was built on unfamiliar territory, claims about system behavior are more likely to be assumptions than verified facts. The scoring pass is cheap — if the plan is genuinely solid, scoring finds nothing and exits quickly
- **Load-bearing external research override:** If Phase 1.4 marked external research as load-bearing (it materially shaped a KTD, Alternative, Scope boundary, or Risk), always proceed to scoring — **even when local implementation patterns are strong**. A landscape or prior-art finding can shape recommendations the local codebase cannot verify, and the thin-grounding override above would miss it. This enters the scoring pass only; it does not force deepening
-If the plan already appears sufficiently grounded and neither the thin-grounding nor the load-bearing-external-research override applies, report "Confidence check passed — no sections need strengthening", then **load `references/plan-handoff.md` now and execute 5.3.8 → 5.3.9 → 5.4 in sequence**. Document review is mandatory for markdown plans and report-only for HTML plans — do not skip it because the confidence check passed. The two tools catch different classes of issues. For HTML plans (`OUTPUT_FORMAT=html`), the plan-handoff 5.3.8 format gate suppresses mutation but still runs ce-doc-review and surfaces findings explicitly.
+If the plan already appears sufficiently grounded and neither the thin-grounding nor the load-bearing-external-research override applies, report "Confidence check passed — no sections need strengthening", then **load `references/plan-handoff.md` now and execute 5.3.8 → 5.3.9 → 5.4 in sequence**. Document review is mandatory for markdown plans and DOM-safe-or-report-only for HTML plans — do not skip it because the confidence check passed. The two tools catch different classes of issues. For HTML plans (`OUTPUT_FORMAT=html`), the plan-handoff 5.3.8 format gate suppresses mutation but still runs ce-doc-review and surfaces findings explicitly.
##### 5.3.3–5.3.7 Deepening Execution
@@ -825,9 +825,9 @@ When deepening is warranted, read `references/deepening-workflow.md` for confide
##### 5.3.8–5.4 Document Review, Final Checks, and Post-Generation Options
-**STOP. Load `references/plan-handoff.md` now before continuing.** It carries the full instructions for 5.3.8 (document review), 5.3.9 (final checks and cleanup), and 5.4 (post-generation handoff, including the Publish to Proof flow and Issue Creation branching). **This load is non-optional** — without it, the agent renders the post-generation menu, captures the user's selection, and stops without firing the routed action. Document review at 5.3.8 runs unconditionally for `OUTPUT_FORMAT=md` and runs report-only for `OUTPUT_FORMAT=html` regardless of whether the confidence check already ran. The default mode is headless (`mode:headless`) — markdown `safe_auto` fixes apply silently, HTML applies no fixes, remaining findings surface contextually above the menu, and a deeper interactive review is opt-in via free-form prompt.
+**STOP. Load `references/plan-handoff.md` now before continuing.** It carries the full instructions for 5.3.8 (document review), 5.3.9 (final checks and cleanup), and 5.4 (post-generation handoff, including the Publish to Proof flow and Issue Creation branching). **This load is non-optional** — without it, the agent renders the post-generation menu, captures the user's selection, and stops without firing the routed action. Document review at 5.3.8 runs unconditionally for `OUTPUT_FORMAT=md` and runs DOM-safe-or-report-only for `OUTPUT_FORMAT=html` regardless of whether the confidence check already ran. The default mode is headless (`mode:headless`) — markdown `safe_auto` fixes apply silently, HTML applies only proven DOM-safe helper fixes, remaining findings surface contextually above the menu, and a deeper interactive review is opt-in via free-form prompt.
-After document review and final checks, print a one-line summary of the headless review state above the menu (e.g., `Doc review applied 3 fixes. 2 decisions, 1 proposed fix, 4 FYI observations remain (1 at P1).`; for HTML plans, print `Doc review (report-only) found N findings; HTML plans are reviewed without autofix.`), then present the menu. Options 1 (`Start /ce-work`) and 2 (`Run it as a /goal`) render only for implementation-ready code plans, and option 2 only on hosts with a top-level `/goal` command (Claude Code and Codex); the `Decide on the review's open items` option renders only when actionable findings remain (`proposed_fixes_count + decisions_count > 0`). FYI-only cases hide it because the walkthrough is gated to actionable findings. HTML cases may show it when actionable findings exist, but the interactive pass remains report-only: present findings and decisions, do not apply fixes or append markdown headings. See `references/plan-handoff.md` for the full rule. When 5 or more options render (exceeding the `AskUserQuestion` 4-option cap), render the menu as a numbered list in chat with the hint "Pick a number or describe what you want." rather than trimming options. On platforms whose blocking question tool has no option cap (Codex `request_user_input`, Pi `ask_user`), use the blocking tool with all rendered options; when it is unavailable or errors (e.g., Codex edit modes), fall back to the same numbered-list-in-chat rendering. When 4 or fewer options render, use the platform's blocking tool (`AskUserQuestion` in Claude Code — call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), with the same numbered-list fallback. Renumber the visible options 1-N. Never silently skip the question.
+After document review and final checks, print a one-line summary of the headless review state above the menu (e.g., `Doc review applied 3 fixes. 2 decisions, 1 proposed fix, 4 FYI observations remain (1 at P1).`; for HTML plans, print `Doc review (DOM-safe/report-only) applied N fixes and found M findings.` when safe fixes land, otherwise `Doc review (report-only) found N findings; HTML plans are reviewed without markdown autofix.`), then present the menu. Options 1 (`Start /ce-work`) and 2 (`Run it as a /goal`) render only for implementation-ready code plans, and option 2 only on hosts with a top-level `/goal` command (Claude Code and Codex); the `Decide on the review's open items` option renders only when actionable findings remain (`proposed_fixes_count + decisions_count > 0`). FYI-only cases hide it because the walkthrough is gated to actionable findings. HTML cases may show it when actionable findings exist, but the interactive pass remains DOM-safe-or-report-only: present findings and decisions, apply only proven helper fixes, and do not append markdown headings. See `references/plan-handoff.md` for the full rule. When 5 or more options render (exceeding the `AskUserQuestion` 4-option cap), render the menu as a numbered list in chat with the hint "Pick a number or describe what you want." rather than trimming options. On platforms whose blocking question tool has no option cap (Codex `request_user_input`, Pi `ask_user`), use the blocking tool with all rendered options; when it is unavailable or errors (e.g., Codex edit modes), fall back to the same numbered-list-in-chat rendering. When 4 or fewer options render, use the platform's blocking tool (`AskUserQuestion` in Claude Code — call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), with the same numbered-list fallback. Renumber the visible options 1-N. Never silently skip the question.
**Question:** "Plan ready at ``. What would you like to do next?" (use absolute path so the reference is clickable in modern terminals)
@@ -837,7 +837,7 @@ After document review and final checks, print a one-line summary of the headless
2. **Run it as a `/goal`** - Run this plan as an autonomous `/goal` to its Definition of Done — fewer check-ins; good for longer or unattended runs. The alternative to option 1, not an add-on — pick one. Implementation-ready code plans only, and only where the host has goal mode (a callable tool like Codex `create_goal`, or a user-typed `/goal` like Claude Code). Where it can start directly, it does; otherwise it hands over a copy-paste prompt.
**Recommended marker (dynamic):** `/goal` is the recommended default when the host supports it — mark option 2 *(recommended)* and leave option 1 unmarked; on hosts without `/goal` (option 2 omitted), mark option 1 *(recommended)* instead. Exactly one option carries it.
-3. **Decide on the review's open items** - Confirm or skip the suggested edits, and settle the judgment calls the auto-pass left for you. (Markdown safe fixes were already applied; HTML reviews are report-only and do not offer apply or Open Questions write-back.)
+3. **Decide on the review's open items** - Confirm or skip the suggested edits, and settle the judgment calls the auto-pass left for you. (Markdown safe fixes were already applied; HTML reviews only apply proven DOM-safe helper fixes and do not offer markdown apply or Open Questions write-back.)
4. **Create Issue** - Create a tracked issue from this plan in your configured issue tracker (e.g., GitHub Issues, Linear, Jira)
5. **Publish to Proof — shareable link** - Publish the plan to Every's Proof editor and get a shareable link to read, comment on, or share with others. One-way: the local plan file stays canonical. **Render only when `OUTPUT_FORMAT=md`.**
5. **Open in browser** - Open the HTML plan file locally for review and sharing. **Render only when `OUTPUT_FORMAT=html`.**
@@ -846,12 +846,12 @@ After document review and final checks, print a one-line summary of the headless
- **Start `/ce-work`** — Offered only when the artifact is `artifact_readiness: implementation-ready` and `execution: code` (not for requirements-only, universal-planning, answer-seeking, or approach-plan outputs). Invoke the `ce-work` skill via the platform's skill-invocation primitive (`Skill` in Claude Code and Codex, the equivalent on Gemini/Pi), passing the plan path as the skill argument; `ce-work` owns engine selection and the tail. If no skill-invocation primitive exists, print the `ce-work` fallback prompt for the user to run. Do not merely tell the user to type `/ce-work` when a skill invocation primitive is available.
- **Run it as a `/goal`** — Offered on the implementation-ready-code gate, and only where the host has goal mode (callable tool or user-typed `/goal`). **`ce-work` does not also run.** Build a **thin** objective from the plan here (not from a doc section), pointing to the plan's sections — do **not** copy its resolved decisions, exact commands, or requirements into the prompt (deletion test: if the draft names a specific command, file path, U-ID dependency, stop condition, or DoD item, cut it — it should read the same for any plan except the path), and carry the PR-precedence line instead of a hardcoded open/don't-open directive: implement `` to its Definition of Done; scan headings, don't read the whole doc; read the Goal Capsule then work units in dependency order with their cited R/F/AE/KTD; run the plan's Verification Contract gates and satisfy each unit's test scenarios; track progress outside the plan file; follow the plan's PR/landing strategy if it defines one, with repo conventions and user preferences overriding it; surface a genuine blocker (changes scope or contradicts the plan) instead of guessing, using judgment on details the plan leaves open. If a callable goal tool is available (Codex `create_goal`), call it with that objective — the session works toward the DoD; do not call `update_goal` (the goal session completes itself). Otherwise (user-typed `/goal` only, e.g. Claude Code), print that objective as a copyable `/goal` prompt for the user to paste, then return to the menu.
-- **Decide on the review's open items** — Re-invoke the `ce-doc-review` skill on the plan path **without** `mode:headless` so the interactive routing question and walkthrough fire for markdown, or the report-only findings review fires for HTML. HTML review presents findings and decisions without apply or Append-to-Open-Questions write-back. After it returns, re-render this menu with refreshed counts so the user can pick a next-stage action.
+- **Decide on the review's open items** — Re-invoke the `ce-doc-review` skill on the plan path **without** `mode:headless` so the interactive routing question and walkthrough fire for markdown, or the DOM-safe-or-report-only findings review fires for HTML. HTML review presents findings and decisions, applies only proven helper fixes, and offers no markdown apply or Append-to-Open-Questions write-back. After it returns, re-render this menu with refreshed counts so the user can pick a next-stage action.
- **Create Issue** — Detect the project tracker from the project instructions already in your context and create the issue from the plan file as described under "Issue Creation" in `references/plan-handoff.md`. Create the issue through whatever interface the tracker actually exposes — `gh` for GitHub when it's installed and authenticated, otherwise GitHub's connector/MCP tool or API; for Linear, a connector/MCP tool, documented API/GraphQL, or a documented CLI (no guaranteed `linear` CLI). Do not treat a missing binary, env var, or unloaded MCP tool as proof the tracker is unavailable. After creation, display the issue URL and ask whether to proceed to `/ce-work` via the platform's blocking question tool.
- **Publish to Proof — shareable link** — Load the `ce-proof` skill to publish the plan: create a shared Proof doc from the plan file (title = plan title; identity `ai:compound-engineering` / `Compound Engineering`), surface the share URL to the user, then return to this menu. One-way publish — the local plan file stays canonical, nothing syncs back. If the upload fails, see the graceful-fallback note in `references/plan-handoff.md`.
- **Open in browser** — Display the absolute path to the `.html` plan file so the user can open it locally. Where the platform exposes a browser-opening primitive (e.g., `open` on macOS, `xdg-open` on Linux, `start` on Windows), the agent may use it; otherwise print the absolute path and let the user open it. Do not invoke `ce-work` from this option — the user picked HTML for review/sharing, not handoff.
-If the user types free-form prompts targeting the findings (e.g., "review", "walk through", "deep review"), route as if they picked `Decide on the review's open items` — fire the skill rather than looping back to the menu. For HTML plans, that free-form route is still report-only and must not offer apply or Append-to-Open-Questions write-back. For other free-text revisions, accept the input and loop back to this menu after applying the revision.
+If the user types free-form prompts targeting the findings (e.g., "review", "walk through", "deep review"), route as if they picked `Decide on the review's open items` — fire the skill rather than looping back to the menu. For HTML plans, that free-form route is still DOM-safe-or-report-only and must not offer markdown apply or Append-to-Open-Questions write-back. For other free-text revisions, accept the input and loop back to this menu after applying the revision.
**Completion check:** This skill is not complete until the post-generation menu above has been presented, the user has selected an action, and the inline routing for that selection has been executed. Presenting the menu and stopping at the user's selection is not completion — fire the routed action.
diff --git a/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/references/html-rendering.md b/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/references/html-rendering.md
index a1701925d4..01d0609722 100644
--- a/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/references/html-rendering.md
+++ b/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/references/html-rendering.md
@@ -10,10 +10,10 @@ content rendered by different skills shares the same HTML principles.
The HTML artifact is the *only* artifact the skill produces for that run —
output mode is exclusive (markdown OR HTML, never both). Downstream
-consumers that read HTML today (`ce-work`, `ce-doc-review` report-only mode,
+consumers that read HTML today (`ce-work`, `ce-doc-review` DOM-safe-or-report-only mode,
human readers) do so directly; the agent-consumability rules below make that
-work. `ce-doc-review` reviews HTML without mutation: markdown autofix and
-Append-to-Open-Questions write-back remain disabled for `.html` artifacts.
+work. `ce-doc-review` may apply only parse5-backed DOM-safe helper mutations; markdown autofix and
+markdown Append-to-Open-Questions write-back remain disabled for `.html` artifacts, and any helper refusal falls back to report-only.
## Hard invariants
@@ -256,6 +256,12 @@ where the doc presents itself as a reference index.
### Stable section anchors for unified plans
+
+
+
When rendering a unified plan, every major logical section gets a stable
anchor ID and visible heading text:
@@ -543,10 +549,10 @@ fine when the content suggests them.
## Agent-consumability rules
Downstream agents that read HTML today (`ce-work`, `ce-doc-review` in
-report-only mode, a skill re-reading its own prior artifact on a resume run,
+DOM-safe-or-report-only mode, a skill re-reading its own prior artifact on a resume run,
future consumers) reason over the HTML as text — the way they reason over
markdown, not via DOM extraction or a script-style parse. `ce-doc-review`
-uses this consumable structure to produce findings without mutating HTML (see
+uses this consumable structure to produce findings and may mutate only through the FN-7149 DOM-safe helper (see
opening note).
These rules are why such a consumer can locate one item (a single
diff --git a/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/references/plan-handoff.md b/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/references/plan-handoff.md
index 2484b6fa65..60e7b2acb2 100644
--- a/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/references/plan-handoff.md
+++ b/plugins/fusion-plugin-compound-engineering/src/skills/ce-plan/references/plan-handoff.md
@@ -4,30 +4,30 @@ This file contains post-plan-writing instructions: document review, post-generat
## 5.3.8 Document Review
-**Format gate.** This phase runs for both markdown and HTML plans. Markdown plans use the full headless `ce-doc-review` path, including safe markdown-only autofixes. HTML plans use `ce-doc-review` in report-only mode: persona lenses run and findings are returned as structured text, but all mutation mechanics are suppressed because `gated_auto`/`manual` apply-set edits and Append-to-Open-Questions write-back are markdown-specific.
+**Format gate.** This phase runs for both markdown and HTML plans. Markdown plans use the full headless `ce-doc-review` path, including safe markdown-only autofixes. HTML plans use `ce-doc-review` with DOM-safe mutation enabled only when the FN-7149 helper proves safety; persona lenses always run, findings are returned as structured text, and any helper refusal falls back to report-only because `gated_auto`/`manual` apply-set edits and markdown Append-to-Open-Questions write-back are markdown-specific.
-**When `OUTPUT_FORMAT=html`:** Run the `ce-doc-review` skill with `mode:headless` on the `.html` plan file and explicitly treat the invocation as report-only. Pass `mode:headless ` as the skill arguments. Capture the returned envelope so the menu summary in 5.4 can name the report-only result:
-- `fixes_applied = 0` / `applied_fixes_count = 0`
+**When `OUTPUT_FORMAT=html`:** Run the `ce-doc-review` skill with `mode:headless` on the `.html` plan file and explicitly treat the invocation as DOM-safe-or-report-only. Pass `mode:headless ` as the skill arguments. Capture the returned envelope so the menu summary in 5.4 can name either the safe mutation result or the report-only fallback:
+- `fixes_applied = 0` / `applied_fixes_count = 0` when the helper refuses or no safe mutation applies
- `proposed_fixes_count`, `decisions_count`, and `fyi_count` from the reviewer output
- no `skipped_reason` field
-Do not block on this — the optional workflow remains advisory — but do surface P0/P1 findings before returning control to the caller. Free-form requests for review in the post-generation menu re-run `ce-doc-review` on the HTML plan in report-only mode (present findings; no autofix; no Append-to-Open-Questions write-back).
+Do not block on this — the optional workflow remains advisory — but do surface P0/P1 findings before returning control to the caller. Free-form requests for review in the post-generation menu re-run `ce-doc-review` on the HTML plan in DOM-safe-or-report-only mode (present findings; apply only proven DOM-safe helper fixes; no markdown autofix; no markdown Append-to-Open-Questions write-back).
**When `OUTPUT_FORMAT=md`:** Run the `ce-doc-review` skill with `mode:headless` on the plan file. Pass `mode:headless ` as the skill arguments. When this step is reached for a markdown plan, it is mandatory — do not skip it because the confidence check already ran. The two tools catch different classes of issues.
-Headless is the default at this phase because most users want to start work after planning, not adjudicate every reviewer concern up front. For markdown, headless applies `safe_auto` fixes silently and returns structured findings text — no walkthrough, no per-finding routing, no blocking prompts. For HTML, headless returns the same structured findings text but applies nothing. The post-generation menu (see 5.4) offers `Decide on the review's open items` as a first-class option so users can opt into the full interactive walkthrough when they want it; for HTML that walkthrough stays report-only and omits apply/Append choices.
+Headless is the default at this phase because most users want to start work after planning, not adjudicate every reviewer concern up front. For markdown, headless applies `safe_auto` fixes silently and returns structured findings text — no walkthrough, no per-finding routing, no blocking prompts. For HTML, headless returns the same structured findings text and may apply only DOM-safe helper fixes; any safety failure applies nothing and reports `fixes_applied = 0`. The post-generation menu (see 5.4) offers `Decide on the review's open items` as a first-class option so users can opt into the full interactive walkthrough when they want it; for HTML that walkthrough stays DOM-safe-or-report-only and omits markdown apply/Append choices.
The confidence check and ce-doc-review are complementary:
- The confidence check strengthens rationale, sequencing, risk treatment, and grounding
- Document-review checks coherence, feasibility, scope alignment, and surfaces role-specific issues
Capture the headless envelope so it can drive the contextual summary above the post-generation menu:
-- The number of fixes auto-applied (always `0` for HTML)
+- The number of fixes auto-applied (`0` for HTML whenever the DOM-safe helper refuses or no allowlisted mutation applies)
- The count of remaining findings, broken out by user-facing bucket (proposed fixes, decisions, FYI observations)
- The severity breakdown of decisions and proposed fixes (specifically the P0/P1 count, since those benefit from explicit user attention)
@@ -46,11 +46,11 @@ If artifact-backed mode was used:
- Clean up the temporary scratch directory after the plan is safely updated
- If cleanup is not practical on the current platform, note where the artifacts were left
-**Format-specific composition.** When `OUTPUT_FORMAT=html` (resolved in SKILL.md Phase 0.0), the plan is written as a single self-contained `.html` file — there is no markdown sibling. Read `references/html-rendering.md` for composition rules: invariants, precedence stack, format principles, agent-consumability rules, and the post-compose audit. The `.html` file is the artifact downstream consumers (ce-work, human readers, and ce-doc-review's report-only path) read. `ce-doc-review` reviews HTML without mutation; its markdown autofix and Open Questions write-back mechanics remain markdown-only.
+**Format-specific composition.** When `OUTPUT_FORMAT=html` (resolved in SKILL.md Phase 0.0), the plan is written as a single self-contained `.html` file — there is no markdown sibling. Read `references/html-rendering.md` for composition rules: invariants, precedence stack, format principles, agent-consumability rules, and the post-compose audit. The `.html` file is the artifact downstream consumers (ce-work, human readers, and ce-doc-review's DOM-safe-or-report-only path) read. `ce-doc-review` reviews HTML with DOM-safe mutation only when the helper proves safety; its markdown autofix and Open Questions write-back mechanics remain markdown-only.
When `OUTPUT_FORMAT=md`, write the markdown directly per `references/markdown-rendering.md`. No HTML is composed.
-After all mutations in this run have settled (initial write, deepening synthesis, ce-doc-review `safe_auto` fixes when `OUTPUT_FORMAT=md`), the artifact at its single path reflects the final state. Publishing to Proof is one-way and does not mutate the local file. HTML runs receive ce-doc-review findings but no autofix mutations (see 5.3.8 format gate).
+After all mutations in this run have settled (initial write, deepening synthesis, ce-doc-review `safe_auto` fixes when `OUTPUT_FORMAT=md`, and any DOM-safe ce-doc-review helper fixes when `OUTPUT_FORMAT=html`), the artifact at its single path reflects the final state. Publishing to Proof is one-way and does not mutate the local file. HTML runs receive ce-doc-review findings and may receive DOM-safe fixes only when validation succeeds (see 5.3.8 format gate).
## 5.4 Post-Generation Options
@@ -58,7 +58,7 @@ After all mutations in this run have settled (initial write, deepening synthesis
**Path format:** Use absolute paths for chat-output file references — relative paths are not auto-linked as clickable in most terminals.
-**Summary line above the menu (always):** Print a single concise line summarizing the headless review state — e.g., `Doc review applied 3 fixes. 2 decisions, 1 proposed fix, 4 FYI observations remain (1 at P1).` When no fixes were applied and no findings remain, print `Doc review clean — no fixes needed.` For HTML envelopes, print `Doc review (report-only) found N findings; HTML plans are reviewed without autofix.` (or `Doc review (report-only) clean — HTML plan reviewed without autofix.` when no findings remain). This line establishes what the autofix/report-only pass did so the user has the context to choose between the menu options below.
+**Summary line above the menu (always):** Print a single concise line summarizing the headless review state — e.g., `Doc review applied 3 fixes. 2 decisions, 1 proposed fix, 4 FYI observations remain (1 at P1).` When no fixes were applied and no findings remain, print `Doc review clean — no fixes needed.` For HTML envelopes, print `Doc review (DOM-safe/report-only) applied N fixes and found M findings.` when safe fixes land; when no fixes land, print `Doc review (report-only) found N findings; HTML plans are reviewed without markdown autofix.` (or `Doc review (report-only) clean — HTML plan reviewed without markdown autofix.` when no findings remain). This line establishes what the autofix/report-only pass did so the user has the context to choose between the menu options below.
**Question:** "Plan ready at ``. What would you like to do next?"
@@ -67,7 +67,7 @@ After all mutations in this run have settled (initial write, deepening synthesis
2. **Run it as a `/goal`** - Run this plan as an autonomous `/goal` to its Definition of Done — fewer check-ins; good for longer or unattended runs. The alternative to option 1, not an add-on — pick one. Show only when (a) the artifact is `artifact_readiness: implementation-ready` plus `execution: code` AND (b) the host has goal mode at all — a callable goal tool (Codex `create_goal`) or a user-typed `/goal` (Claude Code); omit it where neither exists. Where the host can start a goal directly the session begins it immediately; where it cannot, it hands over a copyable `/goal` prompt. See the routing below.
**Recommended marker (dynamic):** `/goal` is the recommended default when its host supports it — render option 2 as **Run it as a `/goal`** *(recommended)* and leave option 1 unmarked. On hosts without `/goal` (option 2 omitted), mark option 1 **Start `/ce-work`** *(recommended)* instead. Exactly one option ever carries *(recommended)*.
-3. **Decide on the review's open items** - Confirm or skip the suggested edits, and settle the judgment calls the auto-pass left for you. (Markdown safe fixes were already applied; HTML reviews are report-only and do not offer apply or Open Questions write-back.)
+3. **Decide on the review's open items** - Confirm or skip the suggested edits, and settle the judgment calls the auto-pass left for you. (Markdown safe fixes were already applied; HTML reviews only apply proven DOM-safe helper fixes and do not offer markdown apply or markdown Open Questions write-back.)
4. **Create Issue** - Create a tracked issue from this plan in your configured issue tracker (e.g., GitHub Issues, Linear, Jira)
5. **Publish to Proof — shareable link** - Publish the plan to Every's Proof editor and get a shareable link to read, comment on, or share with others. One-way: the local plan file stays canonical. **Render only when `OUTPUT_FORMAT=md`.**
5. **Open in browser** - Open the HTML plan file locally for review and sharing. **Render only when `OUTPUT_FORMAT=html`.**
@@ -100,7 +100,7 @@ Based on selection (the bare per-option routing is also stated inline in the SKI
If the upload fails (network error, Proof API down), retry once after a short wait. If it still fails, tell the user the upload didn't succeed and briefly explain why, then return to the options — don't leave them wondering why the option did nothing.
- **Open in browser** -> Display the absolute path to the `.html` plan file so the user can open it locally. Where the platform exposes a browser-opening primitive (e.g., `open` on macOS, `xdg-open` on Linux, `start` on Windows), the agent may invoke it directly; otherwise print the absolute path and let the user open it. After the path is displayed (or the browser is opened), return to the post-generation options so the user can pick a follow-up action.
-- **Free-form prompts that target the findings** (e.g., the user types "review", "walk through", "deep review" instead of picking a numbered option) -> route as if they had picked `Decide on the review's open items`. Do not loop back to the menu without firing the review. For HTML plans, fire ce-doc-review in report-only mode: present findings and decisions, apply nothing, and do not append markdown headings into the HTML artifact.
+- **Free-form prompts that target the findings** (e.g., the user types "review", "walk through", "deep review" instead of picking a numbered option) -> route as if they had picked `Decide on the review's open items`. Do not loop back to the menu without firing the review. For HTML plans, fire ce-doc-review in DOM-safe-or-report-only mode: present findings and decisions, apply only proven helper fixes, and do not append markdown headings into the HTML artifact.
- **Other free-form input** -> Accept revisions to the plan and loop back to options.
## Issue Creation
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e6501164bf..eaee72c2d1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -829,6 +829,9 @@ importers:
lucide-react:
specifier: ^0.542.0
version: 0.542.0(react@19.2.4)
+ parse5:
+ specifier: ^8.0.0
+ version: 8.0.0
react:
specifier: ^19.0.0
version: 19.2.4