diff --git a/skills/fix-title/SKILL.md b/skills/fix-title/SKILL.md index fca01a5..39b8e77 100644 --- a/skills/fix-title/SKILL.md +++ b/skills/fix-title/SKILL.md @@ -1,6 +1,6 @@ --- name: fix-title -description: "Use when copied ChatGPT/LLM Markdown has semantically wrong heading hierarchy, especially discussion records with round headings and GPT reply blocks, or intake/artifact drafts with repeated accidental top-level headings. Agentic Markdown heading repair for one or more files: accept a file path array plus mode discussion or artifact, prefer subagent/thread isolation for large repairs, preserve originals, and produce fixed copies, heading plans, and reports." +description: "Use when copied ChatGPT/LLM Markdown has semantically wrong heading hierarchy, especially discussion records with round headings and GPT reply blocks, or intake/artifact drafts with repeated accidental top-level headings. Agentic Markdown heading repair for one or more files: default to lightweight in-place repair that overwrites the source Markdown without sidecar files; use audit mode only when the caller asks for preserved originals, fixed copies, heading maps, JSON plans, or reports." --- # Fix Title @@ -16,37 +16,113 @@ Required inputs: Optional inputs: -- `output_dir`: output folder. If omitted, use repo-local `tmp/fix-title-output` when available; otherwise use `fix-title-output` beside the first source file. -- `execution`: `auto`, `subagent`, `thread`, or `inline`. Default to `auto`. +- `output`: `quick` or `audit`. Default to `quick`. +- `execution`: `auto`, `subagent`, `thread`, or `inline`. Default to `inline` for quick single-file work and `auto` for audit or large batches. +- `output_dir`: audit output folder. Ignore in quick mode unless the caller also asks for backup or reports. +- `backup`: `never`, `always`, or `auto`. Default to `never`; rely on git unless the caller asks for backups or the file is outside version control. Example user-facing request: ```text -Use $fix-title with mode=artifact on: +Use $fix-title with mode=artifact on C:\path\one.md +``` + +This defaults to quick mode and overwrites `C:\path\one.md` directly. + +Audit example: + +```text +Use $fix-title with mode=artifact and output=audit on: - C:\path\one.md - C:\path\two.md Output to C:\path\tmp\fix-title-output ``` +## Output Modes + +### Quick + +Use quick mode by default. + +- Overwrite each source Markdown file in place. +- Do not write `heading-map.json`, `heading-plan.json`, `.fixed.md`, heading reports, or batch reports. +- Use in-memory edits and `scripts/semantic_heading_repair.py` helpers when practical. +- Return a concise summary with changed heading counts and skipped files. +- If a file is ambiguous, skip it and tell the caller why instead of creating audit artifacts by default. + +Quick mode is for routine cleanup where the caller wants the document fixed, not a review package. + +### Audit + +Use audit mode only when the caller asks for auditability, preserved originals, fixed copies, maps, plans, reports, review evidence, batch reports, or no overwrite. + +For each source file, write: + +```text +.heading-map.json +.heading-plan.json +.fixed.md +.heading-report.md +``` + +For batch runs, also write: + +```text +fix-title-batch-report.md +``` + +The plan JSON is the audit boundary. Every changed heading must include: + +```json +{ + "line": 25, + "from_level": 1, + "to_level": 3, + "reason": "This is a top-level section inside the GPT reply block under ## GPT." +} +``` + +The caller must verify returned files and reports in audit mode. Do not treat a subagent/thread message alone as proof of repair. + ## Execution Isolation -Prefer isolation because semantic repair consumes context and does not depend on the caller's broader conversation. +Quick single-file repairs should usually run inline. Prefer isolation only when semantic repair consumes significant context or the caller asks for it. -- `subagent`: Use when the host supports delegated agents. Pass only the skill path, input files, mode, output directory, and output contract. -- `thread`: Use when the host supports child/background threads and the user explicitly asks for Thread/sub-session isolation or the batch is large enough to risk bloating the main context. -- `inline`: Use only when no subagent/thread mechanism is available or the file is small. -- `auto`: Choose `thread` when explicitly requested, otherwise choose `subagent` when available, otherwise `inline`. - -The caller must verify returned files and reports. Do not treat a subagent/thread message alone as proof of repair. +- `inline`: Use for quick mode and small files. +- `subagent`: Use when the host supports delegated agents and the batch is large or audit mode is requested. +- `thread`: Use when the host supports child/background threads and the user explicitly asks for Thread/sub-session execution. +- `auto`: Choose `thread` when explicitly requested, otherwise choose `subagent` for audit or large batches when available, otherwise `inline`. ## Delegation Prompt -Send this shape to the isolated worker: +For quick mode, send this shape to the isolated worker only when isolation is warranted: ```text Use $fix-title at to repair Markdown heading hierarchy. Mode: +Output: quick +Files: +- +- + +Rules: +- Read and write source files as UTF-8. +- Overwrite each source Markdown file in place. +- Do not write heading-map, heading-plan, fixed-copy, heading-report, or batch-report files. +- Preserve non-heading content. +- Apply only deliberate heading-level edits. +- Flag ambiguous appended artifacts or uncertain heading relationships instead of silently making creative decisions. +- Return changed heading counts and skipped files. +``` + +For audit mode, send this shape: + +```text +Use $fix-title at to repair Markdown heading hierarchy. + +Mode: +Output: audit Output directory: Files: - @@ -87,34 +163,6 @@ Use when copied GPT output is stored as a standalone planning/intake/artifact do Example: after a root title, raw sibling headings `# 1.xxxx`, `# 2.xxx`, and `## 3.xxx` may all become `##` when they are peer sections. -## Output Contract - -For each source file, write: - -```text -.heading-map.json -.heading-plan.json -.fixed.md -.heading-report.md -``` - -For batch runs, also write: - -```text -fix-title-batch-report.md -``` - -The plan JSON is the audit boundary. Every changed heading must include: - -```json -{ - "line": 25, - "from_level": 1, - "to_level": 3, - "reason": "This is a top-level section inside the GPT reply block under ## GPT." -} -``` - ## Internal Helper Use `scripts/semantic_heading_repair.py` only as an internal importable helper. It is not a user-facing CLI. @@ -124,13 +172,16 @@ Useful helper API: - `inspect_payload(path)` - `write_inspection(source, output, output_format="json")` - `prepare_batch_scaffold(files, mode, output_dir)` +- `apply_edits(source, edits, output, report=None, mode="quick")` +- `apply_edits_in_place(source, edits, mode="quick", report=None)` - `apply_plan(source, plan, output, report)` The helper protects fenced code blocks, quoted headings, and indented code. It does not make semantic decisions. ## Repair Rules -- Never overwrite the source file unless the user explicitly asks for replacement. +- Quick mode overwrites the source file by default. +- Audit mode never overwrites the source file unless the caller explicitly asks for replacement after reviewing the fixed copy. - Do not mechanically add or subtract the same number of heading levels across a whole file. - Do not change non-heading content. - Do not change headings inside fenced code blocks, block quotes, or indented code. diff --git a/skills/fix-title/agents/openai.yaml b/skills/fix-title/agents/openai.yaml index d52d4ee..33f577c 100644 --- a/skills/fix-title/agents/openai.yaml +++ b/skills/fix-title/agents/openai.yaml @@ -1,3 +1,3 @@ display_name: "Fix Title" -short_description: "Agentic repair for Markdown heading trees." -default_prompt: "Use $fix-title with files=[...] and mode=artifact or mode=discussion to repair Markdown heading hierarchy without overwriting sources." +short_description: "Lightweight repair for Markdown heading trees." +default_prompt: "Use $fix-title with files=[...] and mode=artifact or mode=discussion to repair Markdown heading hierarchy in place. Ask for output=audit when fixed copies, heading maps, JSON plans, or reports are needed." diff --git a/skills/fix-title/manual-test/README.md b/skills/fix-title/manual-test/README.md index 88d8d8f..b410897 100644 --- a/skills/fix-title/manual-test/README.md +++ b/skills/fix-title/manual-test/README.md @@ -7,9 +7,11 @@ Recommended files: ```text raw-discussion.md raw-artifact.md -heading-plan.json +audit-heading-plan.json ``` -The raw files should preserve original copied LLM/GPT Markdown before heading repair. +The raw files should contain disposable copies of original LLM/GPT Markdown before heading repair. -Invoke the Skill through an agent prompt with `files=[...]`, `mode=discussion|artifact`, and an explicit `output_dir`. Do not overwrite the source during manual validation. +Default manual validation should invoke the Skill with `files=[...]` and `mode=discussion|artifact`, then confirm the source copy was repaired in place without sidecar files. + +Use `output=audit` and an explicit `output_dir` only when validating fixed-copy, heading-map, heading-plan, and report generation. diff --git a/skills/fix-title/scripts/semantic_heading_repair.py b/skills/fix-title/scripts/semantic_heading_repair.py index dcdc3b2..389d909 100644 --- a/skills/fix-title/scripts/semantic_heading_repair.py +++ b/skills/fix-title/scripts/semantic_heading_repair.py @@ -274,14 +274,10 @@ def parse_plan(plan_path: pathlib.Path) -> tuple[str, list[Edit]]: return mode, edits -def apply_plan( - source: pathlib.Path, - plan: pathlib.Path, - output: pathlib.Path, - report: pathlib.Path | None, -) -> int: - lines = source.read_text(encoding="utf-8").splitlines(keepends=True) - mode, edits = parse_plan(plan) +def _apply_edits_to_lines( + lines: list[str], + edits: list[Edit], +) -> list[dict[str, Any]]: headings_by_line = {heading.line: heading for heading in iter_headings(lines)} changed: list[dict[str, Any]] = [] @@ -312,23 +308,63 @@ def apply_plan( } ) + return changed + + +def apply_edits( + source: pathlib.Path, + edits: list[Edit], + output: pathlib.Path, + report: pathlib.Path | None = None, + mode: str = "quick", + plan_label: str = "in-memory edits", +) -> int: + lines = source.read_text(encoding="utf-8").splitlines(keepends=True) + changed = _apply_edits_to_lines(lines, edits) + output.parent.mkdir(parents=True, exist_ok=True) output.write_text("".join(lines), encoding="utf-8") if report is not None: report.parent.mkdir(parents=True, exist_ok=True) report.write_text( - format_report(source, output, plan, mode, changed), + format_report(source, output, plan_label, mode, changed), encoding="utf-8", ) return len(changed) +def apply_edits_in_place( + source: pathlib.Path, + edits: list[Edit], + mode: str = "quick", + report: pathlib.Path | None = None, +) -> int: + return apply_edits( + source=source, + edits=edits, + output=source, + report=report, + mode=mode, + plan_label="in-place edits", + ) + + +def apply_plan( + source: pathlib.Path, + plan: pathlib.Path, + output: pathlib.Path, + report: pathlib.Path | None, +) -> int: + mode, edits = parse_plan(plan) + return apply_edits(source, edits, output, report, mode, str(plan)) + + def format_report( source: pathlib.Path, output: pathlib.Path, - plan: pathlib.Path, + plan: pathlib.Path | str, mode: str, changed: list[dict[str, Any]], ) -> str: diff --git a/skills/fix-title/tests/test_semantic_heading_repair.py b/skills/fix-title/tests/test_semantic_heading_repair.py index 0568448..97a4c5c 100644 --- a/skills/fix-title/tests/test_semantic_heading_repair.py +++ b/skills/fix-title/tests/test_semantic_heading_repair.py @@ -159,6 +159,35 @@ class SemanticHeadingRepairTest(unittest.TestCase): with self.assertRaisesRegex(ValueError, "expected level 1"): repair.apply_plan(source, plan, pathlib.Path(tmp) / "fixed.md", None) + def test_apply_edits_in_place_overwrites_source_without_sidecars(self): + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) + source = root / "source.md" + source.write_text("# Title\n\n# Wrong peer\n", encoding="utf-8") + + changed = repair.apply_edits_in_place( + source, + [ + repair.Edit( + line=3, + from_level=1, + to_level=2, + reason="Artifact sections after the title are children.", + ) + ], + mode="artifact", + ) + + self.assertEqual(changed, 1) + self.assertEqual( + source.read_text(encoding="utf-8"), + "# Title\n\n## Wrong peer\n", + ) + self.assertEqual( + sorted(path.name for path in root.iterdir()), + ["source.md"], + ) + def test_prepare_batch_scaffold_accepts_file_array(self): with tempfile.TemporaryDirectory() as tmp: root = pathlib.Path(tmp)