feat: migrate fix-title skill

This commit is contained in:
wantsong 2026-06-13 23:59:58 +08:00
parent ef50a19d89
commit fa61d46933
16 changed files with 1676 additions and 5 deletions

4
.gitignore vendored
View File

@ -29,6 +29,10 @@ tmp/
temp/
*.log
# Manual Skill test artifacts
skills/*/manual-test/*.md
!skills/*/manual-test/README.md
# Secrets
.env
.env.*

107
docs/installation.md Normal file
View File

@ -0,0 +1,107 @@
# Installation
This repository separates Skill development source from the local public installation surface.
## Paths
Development source:
```text
C:\Users\wangq\Documents\Codex\skills-vault
```
Public local Skill installation surface:
```text
C:\Users\wangq\.agents\skills
```
Agent-specific private Skill folders, such as `C:\Users\wangq\.claude\skills`, should link to the public installation surface instead of linking directly to the development source.
## Conda Environment
Use the shared repository environment by default:
```powershell
conda env create -f environment.yml
conda activate skills-vault
```
If the environment already exists:
```powershell
conda activate skills-vault
```
Most lightweight Skills should use this shared environment. Create a dedicated environment only when a Skill has conflicting or heavy dependencies.
## Install One Skill
PowerShell:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill fix-title
```
Git Bash / Linux / macOS:
```bash
bash scripts/install-skill.sh fix-title
```
## Install All Default Skills
PowerShell:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -All
```
Git Bash / Linux / macOS:
```bash
bash scripts/install-skill.sh --all
```
Full installation uses `registry/skills-index.md` and installs only Skills with:
```text
Status: tested, installable, or installed
Install Policy: default
```
Conditional, optional, disabled, candidate, deprecated, and archived Skills are skipped during full installation.
## Updating An Existing Installed Skill
By default, install scripts refuse to overwrite an existing Skill.
Use `-Force` or `--force` to back up the current installed directory and copy the repository version:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill fix-title -Force
```
```bash
bash scripts/install-skill.sh fix-title --force
```
Backups are created next to the installed Skill:
```text
C:\Users\wangq\.agents\skills\fix-title.backup-YYYYMMDDHHMMSS
```
## Claude Link
To create or update the Claude private Skill entry as a link to the public installation surface:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill fix-title -InstallClaudeLink
```
```bash
bash scripts/install-skill.sh fix-title --install-claude-link
```
Codex is not currently treated as a separate private install target for these shared automation Skills.

6
environment.yml Normal file
View File

@ -0,0 +1,6 @@
name: skills-vault
channels:
- conda-forge
dependencies:
- python=3.11
- pip

View File

@ -11,9 +11,9 @@ Existing CCPE content is not migrated here.
## Skills
| Skill | Purpose | Source | Status | Installed Path | CCPE Registration |
| --- | --- | --- | --- | --- | --- |
| _none yet_ | | | | | |
| Skill | Purpose | Source | Status | Install Policy | Installed Path | CCPE Registration |
| --- | --- | --- | --- | --- | --- | --- |
| `fix-title` | Shift Markdown ATX heading depth for pasted LLM replies before insertion under parent sections. | `C:\Users\wangq\Documents\Codex\knowledge-vault\skills\fix-title` | `installed` | `default` | `C:\Users\wangq\.agents\skills\fix-title` | none |
## Status Values
@ -25,3 +25,12 @@ installed
deprecated
archived
```
## Install Policy Values
```text
default
optional
conditional
disabled
```

View File

@ -6,8 +6,8 @@ Examples of future scripts:
- validate Skill directory structure
- list registered Skills
- install selected Skills to `C:\Users\wangq\.agents\skills`
- sync selected Skills
- install selected or default Skills to `C:\Users\wangq\.agents\skills`
- create Agentic private Skill links that point to `.agents\skills`
- check registry consistency
Skill-specific scripts should live under:
@ -15,3 +15,15 @@ Skill-specific scripts should live under:
```text
skills/<skill-name>/scripts/
```
Current install scripts:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill fix-title
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -All
```
```bash
bash scripts/install-skill.sh fix-title
bash scripts/install-skill.sh --all
```

184
scripts/install-skill.ps1 Normal file
View File

@ -0,0 +1,184 @@
param(
[string]$Skill,
[switch]$All,
[switch]$Force,
[switch]$InstallClaudeLink,
[string]$InstallRoot = "$HOME\.agents\skills",
[string]$ClaudeSkillsRoot = "$HOME\.claude\skills"
)
$ErrorActionPreference = "Stop"
function Show-Usage {
Write-Host "Usage:"
Write-Host " powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill fix-title"
Write-Host " powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -All"
Write-Host ""
Write-Host "Options:"
Write-Host " -Force Back up and replace existing installed Skill"
Write-Host " -InstallClaudeLink Create/update .claude\skills link to .agents\skills"
}
if (($Skill -and $All) -or (-not $Skill -and -not $All)) {
Show-Usage
exit 2
}
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RepoRoot = Resolve-Path (Join-Path $ScriptDir "..")
$SkillsRoot = Join-Path $RepoRoot "skills"
$RegistryPath = Join-Path $RepoRoot "registry\skills-index.md"
function Get-RegistryMeta {
param([string]$Name)
if (-not (Test-Path -LiteralPath $RegistryPath)) {
return $null
}
$escaped = [regex]::Escape("`$Name")
$line = Get-Content -LiteralPath $RegistryPath | Where-Object { $_ -match "^\|\s*``$([regex]::Escape($Name))``\s*\|" } | Select-Object -First 1
if (-not $line) {
return $null
}
$parts = $line.Trim() -split "\|"
$cells = @()
foreach ($part in $parts) {
$cell = $part.Trim()
if ($cell.Length -gt 0) {
$cells += $cell
}
}
if ($cells.Count -lt 7) {
return $null
}
return [PSCustomObject]@{
Skill = $cells[0].Trim("``")
Purpose = $cells[1]
Source = $cells[2]
Status = $cells[3].Trim("``")
InstallPolicy = $cells[4].Trim("``")
InstalledPath = $cells[5]
CcpeRegistration = $cells[6]
}
}
function Get-AllDefaultSkills {
$allowedStatuses = @("tested", "installable", "installed")
$skills = @()
Get-ChildItem -LiteralPath $SkillsRoot -Directory | ForEach-Object {
$skillName = $_.Name
$skillFile = Join-Path $_.FullName "SKILL.md"
if (-not (Test-Path -LiteralPath $skillFile)) {
return
}
$meta = Get-RegistryMeta -Name $skillName
if (-not $meta) {
Write-Host "Skipping ${skillName}: no registry row"
return
}
if ($allowedStatuses -notcontains $meta.Status) {
Write-Host "Skipping ${skillName}: status is $($meta.Status)"
return
}
if ($meta.InstallPolicy -ne "default") {
Write-Host "Skipping ${skillName}: install policy is $($meta.InstallPolicy)"
return
}
$skills += $skillName
}
return $skills
}
function Copy-SkillDirectory {
param(
[string]$Name,
[string]$Source,
[string]$Target
)
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Target) | Out-Null
New-Item -ItemType Directory -Force -Path $Target | Out-Null
Get-ChildItem -Force -LiteralPath $Source | Where-Object {
$_.Name -notin @("manual-test", "__pycache__", ".pytest_cache")
} | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $Target -Recurse -Force
}
}
function Backup-ExistingPath {
param([string]$Path)
$stamp = Get-Date -Format "yyyyMMddHHmmss"
$backup = "$Path.backup-$stamp"
Move-Item -LiteralPath $Path -Destination $backup
return $backup
}
function Install-OneSkill {
param([string]$Name)
if ($Name -notmatch "^[a-z0-9][a-z0-9-]*$") {
throw "Invalid Skill name: $Name"
}
$source = Join-Path $SkillsRoot $Name
$skillFile = Join-Path $source "SKILL.md"
if (-not (Test-Path -LiteralPath $skillFile)) {
throw "Skill source not found: $skillFile"
}
$meta = Get-RegistryMeta -Name $Name
if ($meta -and $meta.InstallPolicy -eq "disabled" -and -not $Force) {
throw "Skill $Name has install policy disabled. Use -Force only if you intentionally want to install it."
}
$target = Join-Path $InstallRoot $Name
if (Test-Path -LiteralPath $target) {
if (-not $Force) {
throw "Installed Skill already exists: $target. Re-run with -Force to back it up and replace it."
}
$backup = Backup-ExistingPath -Path $target
Write-Host "Backed up existing Skill to $backup"
}
Copy-SkillDirectory -Name $Name -Source $source -Target $target
Write-Host "Installed $Name to $target"
if ($InstallClaudeLink) {
New-Item -ItemType Directory -Force -Path $ClaudeSkillsRoot | Out-Null
$claudeTarget = Join-Path $ClaudeSkillsRoot $Name
if (Test-Path -LiteralPath $claudeTarget) {
if (-not $Force) {
throw "Claude Skill link/path already exists: $claudeTarget. Re-run with -Force to back it up and replace it."
}
$backup = Backup-ExistingPath -Path $claudeTarget
Write-Host "Backed up existing Claude path to $backup"
}
New-Item -ItemType Junction -Path $claudeTarget -Target $target | Out-Null
Write-Host "Linked Claude Skill $claudeTarget -> $target"
}
}
if ($All) {
$skillNames = Get-AllDefaultSkills
if ($skillNames.Count -eq 0) {
Write-Host "No default installable Skills found."
exit 0
}
foreach ($name in $skillNames) {
Install-OneSkill -Name $name
}
} else {
Install-OneSkill -Name $Skill
}

220
scripts/install-skill.sh Normal file
View File

@ -0,0 +1,220 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
bash scripts/install-skill.sh fix-title
bash scripts/install-skill.sh --all
Options:
--force Back up and replace existing installed Skill
--install-claude-link Create/update .claude/skills link to .agents/skills
EOF
}
skill=""
install_all=0
force=0
install_claude_link=0
while [[ $# -gt 0 ]]; do
case "$1" in
--all)
install_all=1
shift
;;
--force)
force=1
shift
;;
--install-claude-link)
install_claude_link=1
shift
;;
-h|--help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage
exit 2
;;
*)
if [[ -n "$skill" ]]; then
echo "Only one Skill name may be provided." >&2
usage
exit 2
fi
skill="$1"
shift
;;
esac
done
if [[ "$install_all" -eq 1 && -n "$skill" ]] || [[ "$install_all" -eq 0 && -z "$skill" ]]; then
usage
exit 2
fi
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
skills_root="$repo_root/skills"
registry_path="$repo_root/registry/skills-index.md"
if [[ -n "${USERPROFILE:-}" ]]; then
home_dir="$USERPROFILE"
else
home_dir="$HOME"
fi
install_root="${SKILLS_VAULT_INSTALL_ROOT:-$home_dir/.agents/skills}"
claude_skills_root="${SKILLS_VAULT_CLAUDE_SKILLS_ROOT:-$home_dir/.claude/skills}"
registry_meta() {
local name="$1"
[[ -f "$registry_path" ]] || return 1
grep -E "^\|[[:space:]]*\`$name\`[[:space:]]*\|" "$registry_path" | head -n 1 || true
}
registry_cell() {
local line="$1"
local index="$2"
printf '%s\n' "$line" | awk -F'|' -v i="$index" '{gsub(/^ +| +$/, "", $(i + 1)); gsub(/`/, "", $(i + 1)); print $(i + 1)}'
}
all_default_skills() {
local allowed_statuses=" tested installable installed "
for dir in "$skills_root"/*; do
[[ -d "$dir" ]] || continue
[[ -f "$dir/SKILL.md" ]] || continue
local name
name="$(basename "$dir")"
local line
line="$(registry_meta "$name")"
if [[ -z "$line" ]]; then
echo "Skipping $name: no registry row" >&2
continue
fi
local status policy
status="$(registry_cell "$line" 4)"
policy="$(registry_cell "$line" 5)"
if [[ "$allowed_statuses" != *" $status "* ]]; then
echo "Skipping $name: status is $status" >&2
continue
fi
if [[ "$policy" != "default" ]]; then
echo "Skipping $name: install policy is $policy" >&2
continue
fi
printf '%s\n' "$name"
done
}
backup_existing_path() {
local path="$1"
local stamp
stamp="$(date +%Y%m%d%H%M%S)"
local backup="$path.backup-$stamp"
mv "$path" "$backup"
printf '%s\n' "$backup"
}
copy_skill_directory() {
local source="$1"
local target="$2"
mkdir -p "$target"
shopt -s dotglob nullglob
for item in "$source"/*; do
local base
base="$(basename "$item")"
case "$base" in
manual-test|__pycache__|.pytest_cache)
continue
;;
esac
cp -a "$item" "$target/"
done
shopt -u dotglob nullglob
}
create_claude_link() {
local name="$1"
local target="$2"
local link="$claude_skills_root/$name"
mkdir -p "$claude_skills_root"
if [[ -e "$link" || -L "$link" ]]; then
if [[ "$force" -ne 1 ]]; then
echo "Claude Skill link/path already exists: $link. Re-run with --force to back it up and replace it." >&2
exit 1
fi
local backup
backup="$(backup_existing_path "$link")"
echo "Backed up existing Claude path to $backup"
fi
if [[ "${OS:-}" == "Windows_NT" ]]; then
cmd.exe /c mklink /J "$(cygpath -w "$link")" "$(cygpath -w "$target")" >/dev/null
else
ln -s "$target" "$link"
fi
echo "Linked Claude Skill $link -> $target"
}
install_one_skill() {
local name="$1"
if [[ ! "$name" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
echo "Invalid Skill name: $name" >&2
exit 1
fi
local source="$skills_root/$name"
if [[ ! -f "$source/SKILL.md" ]]; then
echo "Skill source not found: $source/SKILL.md" >&2
exit 1
fi
local line
line="$(registry_meta "$name")"
if [[ -n "$line" ]]; then
local policy
policy="$(registry_cell "$line" 5)"
if [[ "$policy" == "disabled" && "$force" -ne 1 ]]; then
echo "Skill $name has install policy disabled. Use --force only if you intentionally want to install it." >&2
exit 1
fi
fi
local target="$install_root/$name"
mkdir -p "$install_root"
if [[ -e "$target" || -L "$target" ]]; then
if [[ "$force" -ne 1 ]]; then
echo "Installed Skill already exists: $target. Re-run with --force to back it up and replace it." >&2
exit 1
fi
local backup
backup="$(backup_existing_path "$target")"
echo "Backed up existing Skill to $backup"
fi
copy_skill_directory "$source" "$target"
echo "Installed $name to $target"
if [[ "$install_claude_link" -eq 1 ]]; then
create_claude_link "$name" "$target"
fi
}
if [[ "$install_all" -eq 1 ]]; then
mapfile -t skills < <(all_default_skills)
if [[ "${#skills[@]}" -eq 0 ]]; then
echo "No default installable Skills found."
exit 0
fi
for name in "${skills[@]}"; do
install_one_skill "$name"
done
else
install_one_skill "$skill"
fi

View File

@ -0,0 +1,59 @@
# Install fix-title
`fix-title` uses the repository-level `skills-vault` Conda environment.
## Prepare Environment
From the repository root:
```powershell
conda env create -f environment.yml
conda activate skills-vault
```
If the environment already exists:
```powershell
conda activate skills-vault
```
## Test
```powershell
python -m unittest discover -s skills\fix-title\tests -v
```
## Install To Public Skill Surface
PowerShell:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill fix-title
```
Replace an existing installed copy after backing it up:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill fix-title -Force
```
Git Bash / Linux / macOS:
```bash
bash scripts/install-skill.sh fix-title
bash scripts/install-skill.sh fix-title --force
```
## Optional Claude Link
PowerShell:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill fix-title -InstallClaudeLink
```
Git Bash / Linux / macOS:
```bash
bash scripts/install-skill.sh fix-title --install-claude-link
```

View File

@ -0,0 +1,40 @@
# fix-title Migration
## Source
```text
C:\Users\wangq\Documents\Codex\knowledge-vault\skills\fix-title
```
## Target
```text
C:\Users\wangq\Documents\Codex\skills-vault\skills\fix-title
```
## Policy
This migration copies the automation Skill source into `skills-vault`.
The original `knowledge-vault` directory is intentionally left in place. Deleting the old source is a manual user action.
## Install Model
`skills-vault` is the development source repository.
The local public installation surface remains:
```text
C:\Users\wangq\.agents\skills\fix-title
```
Claude, OpenClaw, and other Agentic private Skill folders may link to the public installation surface instead of linking directly to this development source.
## Status
```text
migration status: migrated-source-and-installed
installed version updated: yes
installed backup: C:\Users\wangq\.agents\skills\fix-title.backup-20260613235817
manual test: passed
```

52
skills/fix-title/SKILL.md Normal file
View File

@ -0,0 +1,52 @@
---
name: fix-title
description: Use when a pasted ChatGPT or LLM Markdown reply has heading levels that need to be shifted down before insertion under a parent section such as "## GPT"; also use for Markdown title repair, heading nesting fixes, ATX header adjustment, and adding one or more # levels to headings in a file.
---
# Fix Title
## Overview
Fix Markdown heading depth for copied LLM replies before inserting them into a discussion record. Use the bundled script for deterministic file edits instead of asking the model to rewrite the document.
The default operation adds two heading levels, turning `# Title` into `### Title`, which fits content placed under a parent heading like `## GPT`.
## Workflow
1. Confirm the input is a Markdown file containing the raw reply to adjust.
2. Choose the number of levels to add. Default to `2` unless the user specifies another value.
3. Run `scripts/fix_markdown_titles.py` on the file.
4. If the target is important or the requested level shift is unusual, use `--dry-run` or `--output` first and inspect the result.
5. Report the path modified and the level shift used.
## Commands
```bash
python scripts/fix_markdown_titles.py path/to/reply.md
python scripts/fix_markdown_titles.py path/to/reply.md --levels 1
python scripts/fix_markdown_titles.py path/to/reply.md --levels 3 --dry-run
python scripts/fix_markdown_titles.py path/to/reply.md --levels 2 --output path/to/fixed.md
```
On Windows PowerShell, quote paths that contain spaces:
```powershell
python .\scripts\fix_markdown_titles.py "C:\path with spaces\reply.md" --levels 2
```
## Behavior
| Input | `--levels 2` output |
| --- | --- |
| `# A` | `### A` |
| `## B` | `#### B` |
| `###### C` | `###### C` |
The script only changes ATX headings: lines beginning with up to three spaces followed by `#` through `######` and then whitespace. It does not change headings inside fenced code blocks, quoted lines such as `> # text`, or indented code.
## Common Mistakes
- Do not use a broad replace like `#` to `###`; it will corrupt code blocks, quoted content, tags, and inline text.
- Do not increase headings past level six; Markdown ATX headings stop at `######`.
- Do not change the recorder's outer headings such as `# 1` or `## GPT` unless the user explicitly points the skill at the recorder file and requests that behavior.
- Do not use prompt rewriting for this task when the script can perform the edit directly.

View File

@ -0,0 +1,3 @@
display_name: Fix Markdown Titles
short_description: Shift Markdown headings down before inserting pasted chat replies.
default_prompt: Fix the Markdown heading levels in this file so it can be pasted under a parent section.

View File

@ -0,0 +1,13 @@
# Manual Test Input
Place one raw Markdown file here for manual validation.
Recommended filename:
```text
raw-reply.md
```
The file should be an original pasted LLM reply before heading repair.
After the file is added, run the Skill against a copy or with `--output` first, then compare the result before updating the installed version.

View File

@ -0,0 +1,371 @@
结论:**V4.1 已经基本补齐了前面讨论里最关键的纲领级缺口**,尤其是这五个补得很准:
1. **生产工坊**补上了 `video-workbench / PPT / 多模态素材生产` 这类场景的位置。
2. **QPI Drift Guard**补上了“P 域任务滑入 I 域治理项目”的防线。
3. **Whole-source Gestalt First**吸收了你素材提炼测试里的关键教训。
4. **Execution Authenticity**吸收了第一次多 Agent 评审测试里的“伪执行”教训。
5. **Cost Ledger + Stop Right**吸收了 3000 万 tokens 的 ROI 教训。
这版已经不是“有没有骨架”的问题,而是**有几处术语、边界和强度需要校准**。我会把它分成三类:必须改的小 bug、建议补入纲领的遗漏、建议下放到 Agentic 守则的内容。
---
# 一、必须改的几个小 bug / 强冲突
## 1. “两种封装策略”要改成“三种封装策略”
你第 2 节现在写:
> 根据业务场景的熵值与秩序,我们采用两种不同的封装策略。
但表格已经是三类:
```text
逻辑轮机
生产工坊
战略透镜
```
应改为:
```md
根据业务场景的熵值、秩序与产物形态,我们采用三种不同的封装策略。
```
同时,下面这句也要改:
> 针对 B 类(战略透镜)场景……
因为加了“生产工坊”之后,如果继续按 A/B/C 排列,战略透镜已经不是 B 类,而是 C 类。最稳妥的写法是不要写字母:
```md
针对“战略透镜”场景,当面临试错成本极高、失败即引发法理追责的“单向门”商业决策时……
```
这是小 bug但很重要。分类体系一旦留下歧义后面守则会被带歪。
---
## 2. “Agent 输出必须包含 CoT 摘要”建议改掉
你现在写:
```md
Agent 输出结果时,必须包含 CoT思维链摘要即“我为什么这么判/这么想”
```
这里建议改成:
```md
Agent 输出结果时必须包含可审计的理由摘要Decision Rationale说明其关键依据、适用规则、不确定性、排除选项与可推翻条件。
```
原因不是咬文嚼字。**你真正需要的不是 CoT而是可校准、可审计、可反驳的理由结构。**
CoT 这个词容易把系统引向“暴露内部推理过程”。但真实产品里更稳的形态是:
```text
结论
依据
规则
不确定性
反例
人工确认点
可推翻条件
```
这和你的“钢尺与皮尺”“不仅做对,还要好改”更一致。
---
## 3. QPI 的 I 域响应里,“强制启动双离合榨取”太重
你现在写:
```md
【I】系统响应生态学干预。强制启动后道的“思想考古”与“双离合榨取”……
```
但后面 3.6 又明确说“双离合榨取”是 Beta / 理论沙盘,工程实现成本极高。这两个地方有冲突。
建议改成:
```md
【I】系统响应生态学干预。优先启动思想考古、战略透镜与人机回环仅在高危、非遍历、明确授权的深水区才考虑进入“双离合榨取”或摩擦蒸馏等 Beta 机制。
```
这样层级就清楚了:
```text
I 域默认:思想考古 + 战略透镜 + 人机回环
I 域高危:预设委员会
I 域极高危/实验:双离合榨取
```
否则会把一个 Beta 沙盘机制误升成 I 域默认动作,容易再度重工业化。
---
## 4. “全源完形前置”需要加适用条件
这个原则非常重要,但现在写得有点绝对:
```md
严禁对连贯的长篇语料直接采用基于分块的并发提取机制……
必须首先调用高上下文承载力的主节点……
```
核心判断对,但建议补上适用边界:
```md
当源材料是连贯长篇语料,且仍处于高上下文参与者可承载范围内时,应优先进行 Whole-source Gestalt。若源材料是混合型应先做宏主题拆分若源材料是碎片型应采用平铺发现不强行制造层级。
```
这个边界其实你原来的 runbook 已经有Step 0 会先判断 source 是 coherent、mixed 还是 fragmented并据此选择 structure-first、macro-topic 或 flat-discovery 模式;它还要求在没有真实参与者输出时停止。
所以我建议纲领别写成“所有长材料都必须全源完形”,而是写成:
> **连贯长材料禁止 chunk-first混合材料先宏拆碎片材料不强行升层。**
这更精准。
---
## 5. “确定性的专家能力”建议改成“可校准的专家能力”
第 0 节你写:
```md
通过架构工程封装为确定性的专家能力
```
这句话对逻辑轮机很对但对战略透镜不完全对。I 域本来就不是确定性求解,而是动态权衡、反馈控制、人类裁决。
建议改成:
```md
通过架构工程封装为高保真、可校准、可追溯的专家能力。
```
或者:
```md
在 Q/P 域追求确定性执行,在 I 域追求高保真辅助、责任锚定与动态校准。
```
这样不会和你自己的 QPI / 战略透镜冲突。
---
## 6. “执行真实性”要和“绿野仙踪”显式解耦
你现在同时有:
* 绿野仙踪:人类专家幕后扮演 Agent
* 执行真实性:禁止主控节点代写模拟。
这两条都对,但需要一条桥接规则,否则读者可能觉得矛盾。
建议在第 11 条下面加一句:
```md
绿野仙踪阶段允许人类或主控节点模拟 Agent 以验证价值流,但所有此类产物必须标记为 simulation-only不得作为正式 Agentic 输出进入生产决策链。正式运行阶段必须具备真实 Invocation Record。
```
这很关键。
因为你的第一次流程测试失败,不是因为“模拟”本身不允许,而是因为**模拟产物被当成正式多 Agent 产物**。你原来的 distillation runbook 已经有类似底线:不能把 packet-only 或 prompt-only 当成已执行,主会话不得做 primary source analysis / routing / topic-doc drafting / material extractioncarrier 不可用时要停在 blocked 状态。
所以这里要明确:
```text
模拟可以用于价值验证;
模拟不能冒充正式执行。
```
---
## 7. “绝对停止权”的“代码级”可以稍微下放
你现在写:
```md
系统必须具备代码级的绝对停止权……
```
作为工程守则可以;作为方法论纲领稍微细了点。
建议纲领里写成:
```md
系统必须具备可执行、可审计、可由人类覆盖的停止机制。
```
然后把“代码级熔断、状态锁定、预算上限、自动停止条件”放到 Agentic Engineering Handbook。
更稳的版本:
```md
一旦达到最小充分产物或触发成本 / 范围 / 风险阈值,系统必须停止后续自我扩展,除非人类显式授权升级模式。
```
这样保留原则,又不把实现细节塞进宪法。
---
# 二、我认为还缺的纲领级内容
整体看V4.1 已经很完整。但如果问“是否还有遗漏”,我认为还有 **4 个纲领级补丁** 值得加入。
---
## 1. 增加“方法论与 Agentic 守则的边界”
你前面已经决定拆分:
```text
HiFi Agent Studio = 纲领
Agentic Handbook = 守则
CCPE System = 工坊
Project Runbook = 项目规程
```
但 V4.1 本文里还没有显式声明这个边界。
建议加在开头 Profile 后,或者最后加一节:
```md
## 文档边界
HiFi Agent Studio 是 AI 开发与专家型 Agentic 系统建设的方法论纲领,只规定世界观、产品观、架构底线与最高航行纪律。
具体的 Project Intake、Mode Selector、Invocation Record、Cost Ledger 模板、Authority Matrix、Evaluation Stack、Runtime Maturity Model、平台适配规则应下放至 HiFi Agentic Engineering Handbook 与各项目 Runbook。
当具体工程守则与本纲领冲突时,以本纲领的核心原则为准;当本纲领没有规定时,以守则和项目 Runbook 执行。
```
这个要加。否则 V4.1 会继续向“宪法 + 施工手册混合体”膨胀。
---
## 2. 增加“责任不可外包 / Liability Boundary”
你在 Captain、非遍历性、控制棒里都提到了责任但我建议把它升成航行纪律之一。
可以作为第 14 条:
```md
14. **责任不可外包 (Liability Boundary)**
* *定义*:在 I 域与高风险 P 域AI 可以扩展人类专家的认知半径,但不能替代人类承担最终责任。
* *红线*:任何涉及法理追责、客户重大利益、不可逆决策或高风险判断的系统,必须明确 AI 建议、人类裁决、组织责任与客户责任的边界。禁止用“模型判断”掩盖人的责任坐标。
```
这条应该进纲领,不应只放守则。
因为这是 HiFi Agent Studio 区别于“全自动幻觉派”的价值底线。
---
## 3. 增加“最小权限与副作用隔离”
你在产品定义里提到了 Reactor 里的 Authority但航行纪律里还没有把它作为底线写出来。
Agentic 系统和普通 prompt 最大区别之一是:它能调用工具、改文件、发请求、写数据库、触发外部动作。这里必须有一条纲领级红线。
建议加:
```md
15. **最小权限与副作用隔离 (Least Privilege & Side-effect Isolation)**
* *定义*Agent 的工具权限必须小于其语言能力。能说不代表能做,能建议不代表能执行。
* *红线*:任何具备文件修改、外部 API、数据库写入、消息发送、支付、发布、删除或客户可见输出能力的 Agent必须采用最小权限、沙箱隔离、人工确认与可回滚机制。禁止让通用推理能力直接获得不可逆执行权。
```
这条非常适合你的“物理反应堆”隐喻。
反应堆不是光靠控制棒,还要有阀门、屏蔽层和紧急停堆。
---
## 4. 增加“过程数据的授权与主权”
你有“过程即数据”,也有“双离合榨取 / 隐性遥测”。但这里最好补一条伦理和数据边界。
尤其你未来服务客户,专家修改轨迹、光标悬停、编辑距离、反驳意见,这些都可能是高价值、高敏感数据。
建议在第 3 条“过程即数据”后补一句,或者单独作为第 16 条:
```md
16. **过程数据主权与授权 (Process Data Sovereignty)**
* *定义*:专家的修改痕迹、反馈、犹豫、反驳、编辑轨迹与隐性遥测,是高价值认知资产,也是高敏感数据。
* *红线*:任何过程数据采集必须具备明确授权、用途边界、最小采集、可撤回机制与隔离存储策略。禁止以“系统学习”为名,默认吞并专家经验或客户场景数据。
```
这条会让你的“双离合榨取”更稳,不会显得像认知压榨机。
---
# 三、已有内容中建议弱化或移动的部分
## 1. “双离合榨取”可以保留,但建议再降半档
这部分很有你自己的特色,也确实是战略透镜高危区的未来方向。但它现在语言太硬,比如:
```md
在数学底层上绝对不奖励多数派的平庸共识,精准重奖反共识真理
```
建议改柔一点:
```md
尝试提高少数高质量反共识信号的可见性,避免系统被多数派平庸共识吞没。
```
“反共识”不天然等于“真理”。BTS 是一种机制,不是神谕。这里稍微降调,会让方法论更稳。
---
## 2. “全源完形前置”应该从思想考古里稍微移出来
现在它在 3.2 思想考古下面。我理解你的意图:它来自知识加工和长讨论蒸馏。
但严格说,它更像:
> 上下文工程 / 材料工程原则
而不是思想考古本身。
可以保留在 3.2,但标题可以改成:
```md
3.2 思想考古与上下文完形
```
或者在 3.2 前加一个小节:
```md
3.2 上下文工程Whole-source Gestalt First
3.3 思想考古
```
如果你想保持纲领简洁,当前放法也可以,但建议在表述中明确它是“长材料处理原则”,不是所有思想考古任务的默认动作。
---
## 3. “实测去魅”的例子不要只写专家相似度
你现在写:
```md
构建自动化评测集100 道真题用数据85% 专家相似度)说话。
```
这对逻辑轮机很好,但对战略透镜不够。
建议改成:
```md
构建分层评测:对逻辑轮机使用标准答案、专家相似度与自动化测试;对生产工坊使用可用性、转化效率与返工率;对战略透镜使用决策效用、可校准性、反例处理与治理可追踪性。
```
这样不会把所有 HiFi 系统都压成“准确率系统”。
---

View File

@ -0,0 +1,375 @@
# 1
I said .....
## GPT
结论:**V4.1 已经基本补齐了前面讨论里最关键的纲领级缺口**,尤其是这五个补得很准:
1. **生产工坊**补上了 `video-workbench / PPT / 多模态素材生产` 这类场景的位置。
2. **QPI Drift Guard**补上了“P 域任务滑入 I 域治理项目”的防线。
3. **Whole-source Gestalt First**吸收了你素材提炼测试里的关键教训。
4. **Execution Authenticity**吸收了第一次多 Agent 评审测试里的“伪执行”教训。
5. **Cost Ledger + Stop Right**吸收了 3000 万 tokens 的 ROI 教训。
这版已经不是“有没有骨架”的问题,而是**有几处术语、边界和强度需要校准**。我会把它分成三类:必须改的小 bug、建议补入纲领的遗漏、建议下放到 Agentic 守则的内容。
---
### 一、必须改的几个小 bug / 强冲突
#### 1. “两种封装策略”要改成“三种封装策略”
你第 2 节现在写:
> 根据业务场景的熵值与秩序,我们采用两种不同的封装策略。
但表格已经是三类:
```text
逻辑轮机
生产工坊
战略透镜
```
应改为:
```md
根据业务场景的熵值、秩序与产物形态,我们采用三种不同的封装策略。
```
同时,下面这句也要改:
> 针对 B 类(战略透镜)场景……
因为加了“生产工坊”之后,如果继续按 A/B/C 排列,战略透镜已经不是 B 类,而是 C 类。最稳妥的写法是不要写字母:
```md
针对“战略透镜”场景,当面临试错成本极高、失败即引发法理追责的“单向门”商业决策时……
```
这是小 bug但很重要。分类体系一旦留下歧义后面守则会被带歪。
---
#### 2. “Agent 输出必须包含 CoT 摘要”建议改掉
你现在写:
```md
Agent 输出结果时,必须包含 CoT思维链摘要即“我为什么这么判/这么想”
```
这里建议改成:
```md
Agent 输出结果时必须包含可审计的理由摘要Decision Rationale说明其关键依据、适用规则、不确定性、排除选项与可推翻条件。
```
原因不是咬文嚼字。**你真正需要的不是 CoT而是可校准、可审计、可反驳的理由结构。**
CoT 这个词容易把系统引向“暴露内部推理过程”。但真实产品里更稳的形态是:
```text
结论
依据
规则
不确定性
反例
人工确认点
可推翻条件
```
这和你的“钢尺与皮尺”“不仅做对,还要好改”更一致。
---
#### 3. QPI 的 I 域响应里,“强制启动双离合榨取”太重
你现在写:
```md
【I】系统响应生态学干预。强制启动后道的“思想考古”与“双离合榨取”……
```
但后面 3.6 又明确说“双离合榨取”是 Beta / 理论沙盘,工程实现成本极高。这两个地方有冲突。
建议改成:
```md
【I】系统响应生态学干预。优先启动思想考古、战略透镜与人机回环仅在高危、非遍历、明确授权的深水区才考虑进入“双离合榨取”或摩擦蒸馏等 Beta 机制。
```
这样层级就清楚了:
```text
I 域默认:思想考古 + 战略透镜 + 人机回环
I 域高危:预设委员会
I 域极高危/实验:双离合榨取
```
否则会把一个 Beta 沙盘机制误升成 I 域默认动作,容易再度重工业化。
---
#### 4. “全源完形前置”需要加适用条件
这个原则非常重要,但现在写得有点绝对:
```md
严禁对连贯的长篇语料直接采用基于分块的并发提取机制……
必须首先调用高上下文承载力的主节点……
```
核心判断对,但建议补上适用边界:
```md
当源材料是连贯长篇语料,且仍处于高上下文参与者可承载范围内时,应优先进行 Whole-source Gestalt。若源材料是混合型应先做宏主题拆分若源材料是碎片型应采用平铺发现不强行制造层级。
```
这个边界其实你原来的 runbook 已经有Step 0 会先判断 source 是 coherent、mixed 还是 fragmented并据此选择 structure-first、macro-topic 或 flat-discovery 模式;它还要求在没有真实参与者输出时停止。
所以我建议纲领别写成“所有长材料都必须全源完形”,而是写成:
> **连贯长材料禁止 chunk-first混合材料先宏拆碎片材料不强行升层。**
这更精准。
---
#### 5. “确定性的专家能力”建议改成“可校准的专家能力”
第 0 节你写:
```md
通过架构工程封装为确定性的专家能力
```
这句话对逻辑轮机很对但对战略透镜不完全对。I 域本来就不是确定性求解,而是动态权衡、反馈控制、人类裁决。
建议改成:
```md
通过架构工程封装为高保真、可校准、可追溯的专家能力。
```
或者:
```md
在 Q/P 域追求确定性执行,在 I 域追求高保真辅助、责任锚定与动态校准。
```
这样不会和你自己的 QPI / 战略透镜冲突。
---
#### 6. “执行真实性”要和“绿野仙踪”显式解耦
你现在同时有:
* 绿野仙踪:人类专家幕后扮演 Agent
* 执行真实性:禁止主控节点代写模拟。
这两条都对,但需要一条桥接规则,否则读者可能觉得矛盾。
建议在第 11 条下面加一句:
```md
绿野仙踪阶段允许人类或主控节点模拟 Agent 以验证价值流,但所有此类产物必须标记为 simulation-only不得作为正式 Agentic 输出进入生产决策链。正式运行阶段必须具备真实 Invocation Record。
```
这很关键。
因为你的第一次流程测试失败,不是因为“模拟”本身不允许,而是因为**模拟产物被当成正式多 Agent 产物**。你原来的 distillation runbook 已经有类似底线:不能把 packet-only 或 prompt-only 当成已执行,主会话不得做 primary source analysis / routing / topic-doc drafting / material extractioncarrier 不可用时要停在 blocked 状态。
所以这里要明确:
```text
模拟可以用于价值验证;
模拟不能冒充正式执行。
```
---
#### 7. “绝对停止权”的“代码级”可以稍微下放
你现在写:
```md
系统必须具备代码级的绝对停止权……
```
作为工程守则可以;作为方法论纲领稍微细了点。
建议纲领里写成:
```md
系统必须具备可执行、可审计、可由人类覆盖的停止机制。
```
然后把“代码级熔断、状态锁定、预算上限、自动停止条件”放到 Agentic Engineering Handbook。
更稳的版本:
```md
一旦达到最小充分产物或触发成本 / 范围 / 风险阈值,系统必须停止后续自我扩展,除非人类显式授权升级模式。
```
这样保留原则,又不把实现细节塞进宪法。
---
### 二、我认为还缺的纲领级内容
整体看V4.1 已经很完整。但如果问“是否还有遗漏”,我认为还有 **4 个纲领级补丁** 值得加入。
---
#### 1. 增加“方法论与 Agentic 守则的边界”
你前面已经决定拆分:
```text
HiFi Agent Studio = 纲领
Agentic Handbook = 守则
CCPE System = 工坊
Project Runbook = 项目规程
```
但 V4.1 本文里还没有显式声明这个边界。
建议加在开头 Profile 后,或者最后加一节:
```md
## 文档边界
HiFi Agent Studio 是 AI 开发与专家型 Agentic 系统建设的方法论纲领,只规定世界观、产品观、架构底线与最高航行纪律。
具体的 Project Intake、Mode Selector、Invocation Record、Cost Ledger 模板、Authority Matrix、Evaluation Stack、Runtime Maturity Model、平台适配规则应下放至 HiFi Agentic Engineering Handbook 与各项目 Runbook。
当具体工程守则与本纲领冲突时,以本纲领的核心原则为准;当本纲领没有规定时,以守则和项目 Runbook 执行。
```
这个要加。否则 V4.1 会继续向“宪法 + 施工手册混合体”膨胀。
---
#### 2. 增加“责任不可外包 / Liability Boundary”
你在 Captain、非遍历性、控制棒里都提到了责任但我建议把它升成航行纪律之一。
可以作为第 14 条:
```md
14. **责任不可外包 (Liability Boundary)**
* *定义*:在 I 域与高风险 P 域AI 可以扩展人类专家的认知半径,但不能替代人类承担最终责任。
* *红线*:任何涉及法理追责、客户重大利益、不可逆决策或高风险判断的系统,必须明确 AI 建议、人类裁决、组织责任与客户责任的边界。禁止用“模型判断”掩盖人的责任坐标。
```
这条应该进纲领,不应只放守则。
因为这是 HiFi Agent Studio 区别于“全自动幻觉派”的价值底线。
---
#### 3. 增加“最小权限与副作用隔离”
你在产品定义里提到了 Reactor 里的 Authority但航行纪律里还没有把它作为底线写出来。
Agentic 系统和普通 prompt 最大区别之一是:它能调用工具、改文件、发请求、写数据库、触发外部动作。这里必须有一条纲领级红线。
建议加:
```md
15. **最小权限与副作用隔离 (Least Privilege & Side-effect Isolation)**
* *定义*Agent 的工具权限必须小于其语言能力。能说不代表能做,能建议不代表能执行。
* *红线*:任何具备文件修改、外部 API、数据库写入、消息发送、支付、发布、删除或客户可见输出能力的 Agent必须采用最小权限、沙箱隔离、人工确认与可回滚机制。禁止让通用推理能力直接获得不可逆执行权。
```
这条非常适合你的“物理反应堆”隐喻。
反应堆不是光靠控制棒,还要有阀门、屏蔽层和紧急停堆。
---
#### 4. 增加“过程数据的授权与主权”
你有“过程即数据”,也有“双离合榨取 / 隐性遥测”。但这里最好补一条伦理和数据边界。
尤其你未来服务客户,专家修改轨迹、光标悬停、编辑距离、反驳意见,这些都可能是高价值、高敏感数据。
建议在第 3 条“过程即数据”后补一句,或者单独作为第 16 条:
```md
16. **过程数据主权与授权 (Process Data Sovereignty)**
* *定义*:专家的修改痕迹、反馈、犹豫、反驳、编辑轨迹与隐性遥测,是高价值认知资产,也是高敏感数据。
* *红线*:任何过程数据采集必须具备明确授权、用途边界、最小采集、可撤回机制与隔离存储策略。禁止以“系统学习”为名,默认吞并专家经验或客户场景数据。
```
这条会让你的“双离合榨取”更稳,不会显得像认知压榨机。
---
### 三、已有内容中建议弱化或移动的部分
#### 1. “双离合榨取”可以保留,但建议再降半档
这部分很有你自己的特色,也确实是战略透镜高危区的未来方向。但它现在语言太硬,比如:
```md
在数学底层上绝对不奖励多数派的平庸共识,精准重奖反共识真理
```
建议改柔一点:
```md
尝试提高少数高质量反共识信号的可见性,避免系统被多数派平庸共识吞没。
```
“反共识”不天然等于“真理”。BTS 是一种机制,不是神谕。这里稍微降调,会让方法论更稳。
---
#### 2. “全源完形前置”应该从思想考古里稍微移出来
现在它在 3.2 思想考古下面。我理解你的意图:它来自知识加工和长讨论蒸馏。
但严格说,它更像:
> 上下文工程 / 材料工程原则
而不是思想考古本身。
可以保留在 3.2,但标题可以改成:
```md
3.2 思想考古与上下文完形
```
或者在 3.2 前加一个小节:
```md
3.2 上下文工程Whole-source Gestalt First
3.3 思想考古
```
如果你想保持纲领简洁,当前放法也可以,但建议在表述中明确它是“长材料处理原则”,不是所有思想考古任务的默认动作。
---
#### 3. “实测去魅”的例子不要只写专家相似度
你现在写:
```md
构建自动化评测集100 道真题用数据85% 专家相似度)说话。
```
这对逻辑轮机很好,但对战略透镜不够。
建议改成:
```md
构建分层评测:对逻辑轮机使用标准答案、专家相似度与自动化测试;对生产工坊使用可用性、转化效率与返工率;对战略透镜使用决策效用、可校准性、反例处理与治理可追踪性。
```
这样不会把所有 HiFi 系统都压成“准确率系统”。

View File

@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Shift Markdown ATX headings down by a fixed number of levels."""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
HEADING_RE = re.compile(r"^( {0,3})(#{1,6})([ \t]+.*)$")
FENCE_RE = re.compile(r"^( {0,3})(`{3,}|~{3,})")
def shift_headings(text: str, levels: int) -> str:
if levels <= 0:
raise ValueError("levels must be a positive integer")
lines = text.splitlines(keepends=True)
in_fence = False
fence_marker = ""
output: list[str] = []
for line in lines:
body = line[:-1] if line.endswith("\n") else line
newline = "\n" if line.endswith("\n") else ""
if body.endswith("\r"):
body = body[:-1]
newline = "\r" + newline
fence_match = FENCE_RE.match(body)
if fence_match:
marker = fence_match.group(2)
marker_char = marker[0]
if not in_fence:
in_fence = True
fence_marker = marker_char * len(marker)
elif marker_char == fence_marker[0] and len(marker) >= len(fence_marker):
in_fence = False
fence_marker = ""
output.append(line)
continue
if in_fence:
output.append(line)
continue
heading_match = HEADING_RE.match(body)
if heading_match:
indent, hashes, rest = heading_match.groups()
new_level = min(6, len(hashes) + levels)
output.append(f"{indent}{'#' * new_level}{rest}{newline}")
else:
output.append(line)
return "".join(output)
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Shift Markdown ATX headings down by adding # characters."
)
parser.add_argument("path", type=pathlib.Path, help="Markdown file to rewrite")
parser.add_argument(
"-l",
"--levels",
type=int,
default=2,
help="Number of heading levels to add. Default: 2",
)
parser.add_argument(
"-o",
"--output",
type=pathlib.Path,
help="Write to this file instead of modifying the source file",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print converted content to stdout without writing files",
)
return parser.parse_args(argv)
def main(argv: list[str]) -> int:
args = parse_args(argv)
if args.levels <= 0:
print("error: --levels must be a positive integer", file=sys.stderr)
return 2
try:
source = args.path.read_text(encoding="utf-8")
except OSError as exc:
print(f"error: cannot read {args.path}: {exc}", file=sys.stderr)
return 1
fixed = shift_headings(source, args.levels)
if args.dry_run:
sys.stdout.write(fixed)
return 0
destination = args.output or args.path
try:
destination.write_text(fixed, encoding="utf-8")
except OSError as exc:
print(f"error: cannot write {destination}: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))

View File

@ -0,0 +1,100 @@
import pathlib
import subprocess
import sys
import tempfile
import unittest
SCRIPT = pathlib.Path(__file__).resolve().parents[1] / "scripts" / "fix_markdown_titles.py"
def run_cli(*args):
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
text=True,
capture_output=True,
check=False,
)
class FixMarkdownTitlesTest(unittest.TestCase):
def test_shifts_atx_headings_and_leaves_code_blocks(self):
with tempfile.TemporaryDirectory() as tmp:
target = pathlib.Path(tmp) / "reply.md"
target.write_text(
"\n".join(
[
"Intro",
"",
"# One",
"## Two ##",
"```md",
"# Not a heading",
"```",
"> # quoted content",
" # indented code",
"###### Six",
"",
]
),
encoding="utf-8",
)
result = run_cli(str(target), "--levels", "2")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
target.read_text(encoding="utf-8"),
"\n".join(
[
"Intro",
"",
"### One",
"#### Two ##",
"```md",
"# Not a heading",
"```",
"> # quoted content",
" # indented code",
"###### Six",
"",
]
),
)
def test_dry_run_prints_without_writing(self):
with tempfile.TemporaryDirectory() as tmp:
target = pathlib.Path(tmp) / "reply.md"
target.write_text("# One\n", encoding="utf-8")
result = run_cli(str(target), "--levels", "1", "--dry-run")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "## One\n")
self.assertEqual(target.read_text(encoding="utf-8"), "# One\n")
def test_output_writes_to_separate_file(self):
with tempfile.TemporaryDirectory() as tmp:
source = pathlib.Path(tmp) / "reply.md"
output = pathlib.Path(tmp) / "fixed.md"
source.write_text("# One\n", encoding="utf-8")
result = run_cli(str(source), "--levels", "3", "--output", str(output))
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(source.read_text(encoding="utf-8"), "# One\n")
self.assertEqual(output.read_text(encoding="utf-8"), "#### One\n")
def test_rejects_non_positive_levels(self):
with tempfile.TemporaryDirectory() as tmp:
target = pathlib.Path(tmp) / "reply.md"
target.write_text("# One\n", encoding="utf-8")
result = run_cli(str(target), "--levels", "0")
self.assertNotEqual(result.returncode, 0)
self.assertIn("positive integer", result.stderr)
if __name__ == "__main__":
unittest.main()