feat: make fix-title lightweight by default
This commit is contained in:
parent
19c84ffc8c
commit
15ce3c0834
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: fix-title
|
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
|
# Fix Title
|
||||||
|
|
@ -16,37 +16,113 @@ Required inputs:
|
||||||
|
|
||||||
Optional 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.
|
- `output`: `quick` or `audit`. Default to `quick`.
|
||||||
- `execution`: `auto`, `subagent`, `thread`, or `inline`. Default to `auto`.
|
- `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:
|
Example user-facing request:
|
||||||
|
|
||||||
```text
|
```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\one.md
|
||||||
- C:\path\two.md
|
- C:\path\two.md
|
||||||
Output to C:\path\tmp\fix-title-output
|
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
|
## 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.
|
- `inline`: Use for quick mode and small files.
|
||||||
- `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.
|
- `subagent`: Use when the host supports delegated agents and the batch is large or audit mode is requested.
|
||||||
- `inline`: Use only when no subagent/thread mechanism is available or the file is small.
|
- `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` when available, otherwise `inline`.
|
- `auto`: Choose `thread` when explicitly requested, otherwise choose `subagent` for audit or large batches when available, otherwise `inline`.
|
||||||
|
|
||||||
The caller must verify returned files and reports. Do not treat a subagent/thread message alone as proof of repair.
|
|
||||||
|
|
||||||
## Delegation Prompt
|
## 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
|
```text
|
||||||
Use $fix-title at <skill-path> to repair Markdown heading hierarchy.
|
Use $fix-title at <skill-path> to repair Markdown heading hierarchy.
|
||||||
|
|
||||||
Mode: <discussion|artifact>
|
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>
|
Output directory: <output-dir>
|
||||||
Files:
|
Files:
|
||||||
- <absolute-path-1>
|
- <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.
|
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
|
## Internal Helper
|
||||||
|
|
||||||
Use `scripts/semantic_heading_repair.py` only as an internal importable helper. It is not a user-facing CLI.
|
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)`
|
- `inspect_payload(path)`
|
||||||
- `write_inspection(source, output, output_format="json")`
|
- `write_inspection(source, output, output_format="json")`
|
||||||
- `prepare_batch_scaffold(files, mode, output_dir)`
|
- `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)`
|
- `apply_plan(source, plan, output, report)`
|
||||||
|
|
||||||
The helper protects fenced code blocks, quoted headings, and indented code. It does not make semantic decisions.
|
The helper protects fenced code blocks, quoted headings, and indented code. It does not make semantic decisions.
|
||||||
|
|
||||||
## Repair Rules
|
## 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 mechanically add or subtract the same number of heading levels across a whole file.
|
||||||
- Do not change non-heading content.
|
- Do not change non-heading content.
|
||||||
- Do not change headings inside fenced code blocks, block quotes, or indented code.
|
- Do not change headings inside fenced code blocks, block quotes, or indented code.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
display_name: "Fix Title"
|
display_name: "Fix Title"
|
||||||
short_description: "Agentic repair for Markdown heading trees."
|
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 without overwriting sources."
|
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."
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ Recommended files:
|
||||||
```text
|
```text
|
||||||
raw-discussion.md
|
raw-discussion.md
|
||||||
raw-artifact.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.
|
||||||
|
|
|
||||||
|
|
@ -274,14 +274,10 @@ def parse_plan(plan_path: pathlib.Path) -> tuple[str, list[Edit]]:
|
||||||
return mode, edits
|
return mode, edits
|
||||||
|
|
||||||
|
|
||||||
def apply_plan(
|
def _apply_edits_to_lines(
|
||||||
source: pathlib.Path,
|
lines: list[str],
|
||||||
plan: pathlib.Path,
|
edits: list[Edit],
|
||||||
output: pathlib.Path,
|
) -> list[dict[str, Any]]:
|
||||||
report: pathlib.Path | None,
|
|
||||||
) -> int:
|
|
||||||
lines = source.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
||||||
mode, edits = parse_plan(plan)
|
|
||||||
headings_by_line = {heading.line: heading for heading in iter_headings(lines)}
|
headings_by_line = {heading.line: heading for heading in iter_headings(lines)}
|
||||||
changed: list[dict[str, Any]] = []
|
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.parent.mkdir(parents=True, exist_ok=True)
|
||||||
output.write_text("".join(lines), encoding="utf-8")
|
output.write_text("".join(lines), encoding="utf-8")
|
||||||
|
|
||||||
if report is not None:
|
if report is not None:
|
||||||
report.parent.mkdir(parents=True, exist_ok=True)
|
report.parent.mkdir(parents=True, exist_ok=True)
|
||||||
report.write_text(
|
report.write_text(
|
||||||
format_report(source, output, plan, mode, changed),
|
format_report(source, output, plan_label, mode, changed),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
return len(changed)
|
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(
|
def format_report(
|
||||||
source: pathlib.Path,
|
source: pathlib.Path,
|
||||||
output: pathlib.Path,
|
output: pathlib.Path,
|
||||||
plan: pathlib.Path,
|
plan: pathlib.Path | str,
|
||||||
mode: str,
|
mode: str,
|
||||||
changed: list[dict[str, Any]],
|
changed: list[dict[str, Any]],
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,35 @@ class SemanticHeadingRepairTest(unittest.TestCase):
|
||||||
with self.assertRaisesRegex(ValueError, "expected level 1"):
|
with self.assertRaisesRegex(ValueError, "expected level 1"):
|
||||||
repair.apply_plan(source, plan, pathlib.Path(tmp) / "fixed.md", None)
|
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):
|
def test_prepare_batch_scaffold_accepts_file_array(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = pathlib.Path(tmp)
|
root = pathlib.Path(tmp)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue