feat: make fix-title lightweight by default

This commit is contained in:
wantsong 2026-06-25 09:43:23 +08:00
parent 19c84ffc8c
commit 15ce3c0834
5 changed files with 174 additions and 56 deletions

View File

@ -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
<stem>.heading-map.json
<stem>.heading-plan.json
<stem>.fixed.md
<stem>.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 <skill-path> to repair Markdown heading hierarchy.
Mode: <discussion|artifact>
Output: quick
Files:
- <absolute-path-1>
- <absolute-path-2>
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 <skill-path> to repair Markdown heading hierarchy.
Mode: <discussion|artifact>
Output: audit
Output directory: <output-dir>
Files:
- <absolute-path-1>
@ -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
<stem>.heading-map.json
<stem>.heading-plan.json
<stem>.fixed.md
<stem>.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.

View File

@ -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."

View File

@ -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.

View File

@ -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:

View File

@ -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)