flower-trellis 0.6.0-beta.3 → 0.6.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/enhancements/0.6/.agents/skills/trellis-worktree/SKILL.md +56 -0
- package/enhancements/0.6/.claude/skills/trellis-worktree/SKILL.md +56 -0
- package/enhancements/0.6/overrides/conflicts.json +3 -0
- package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/patch.json +19 -0
- package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/worktree-root-content.py +70 -0
- package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/worktree-root-selector.py +12 -0
- package/enhancements/0.6/scripts/untracked_flow.py +78 -3
- package/enhancements/0.6/scripts/worktree_setup.py +422 -0
- package/enhancements/MANIFEST.json +10 -5
- package/enhancements/common/.common/.claude/skills/humanize-writing/SKILL.md +140 -16
- package/enhancements/common/.common/.codex/skills/humanize-writing/SKILL.md +140 -16
- package/package.json +3 -3
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trellis-worktree
|
|
3
|
+
description: "Prepare and diagnose Trellis usage inside linked Git worktrees. Use when the user mentions worktree, linked worktree, worktree development, missing .trellis in a worktree, or asks to make .claude/.codex/.agents/Trellis files work from a worktree."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Trellis Worktree
|
|
7
|
+
|
|
8
|
+
Use this skill before normal Trellis routing when the user's current request is about using Trellis from a linked Git worktree.
|
|
9
|
+
|
|
10
|
+
The problem is usually not task or untracked routing itself: linked worktrees often do not contain the untracked `.trellis`, `.agents`, `.codex`, or `.claude` directories that AI tools need before any Trellis hook or skill can run.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Identify the target worktree. Use the user's explicit path if present; otherwise use the current working directory.
|
|
15
|
+
2. Locate a source worktree in the same Git worktree set that contains `.trellis/scripts/worktree_setup.py`.
|
|
16
|
+
3. Run read-only diagnosis first:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python3 <source-worktree>/.trellis/scripts/worktree_setup.py status --target <target-worktree> --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
4. If status is `needs-prepare`, run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 <source-worktree>/.trellis/scripts/worktree_setup.py prepare --target <target-worktree> --json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
5. If status is `blocked` or `error`, stop and report the reason and conflict paths. Do not overwrite or delete user files.
|
|
29
|
+
6. After `ready` or `prepared`, return to the user's original Trellis intent and follow the normal workflow state.
|
|
30
|
+
|
|
31
|
+
## Source Worktree Discovery
|
|
32
|
+
|
|
33
|
+
If `<source-worktree>` is not obvious, inspect Git worktrees:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git -C <target-worktree> worktree list --porcelain
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Choose a listed worktree that contains `.trellis/scripts/worktree_setup.py`. If none exists, report that this project has not been updated with `trellis-worktree` support yet.
|
|
40
|
+
|
|
41
|
+
## Safety Rules
|
|
42
|
+
|
|
43
|
+
- The helper only projects source paths that already exist.
|
|
44
|
+
- MVP projection paths are `.trellis`, `.agents`, `.codex`, and `.claude`.
|
|
45
|
+
- Projection uses symlinks and records `<target-worktree>/.trellis-worktree.json`.
|
|
46
|
+
- Existing non-managed `.codex`, `.claude`, `.agents`, or `.trellis` paths are conflicts.
|
|
47
|
+
- Do not hand-copy platform directories. Copying creates drift between worktrees.
|
|
48
|
+
- Do not treat successful setup as task approval, check approval, or push approval.
|
|
49
|
+
|
|
50
|
+
## Expected Results
|
|
51
|
+
|
|
52
|
+
- `ready`: target worktree already has valid Trellis entry points.
|
|
53
|
+
- `needs-prepare`: target worktree is safe to prepare; run `prepare`.
|
|
54
|
+
- `prepared`: symlinks and manifest were created or repaired.
|
|
55
|
+
- `blocked`: user-owned paths would be overwritten; stop and report `conflicts`.
|
|
56
|
+
- `error`: Git or source-root resolution failed; stop and report `reason`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trellis-worktree
|
|
3
|
+
description: "Prepare and diagnose Trellis usage inside linked Git worktrees. Use when the user mentions worktree, linked worktree, worktree development, missing .trellis in a worktree, or asks to make .claude/.codex/.agents/Trellis files work from a worktree."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Trellis Worktree
|
|
7
|
+
|
|
8
|
+
Use this skill before normal Trellis routing when the user's current request is about using Trellis from a linked Git worktree.
|
|
9
|
+
|
|
10
|
+
The problem is usually not task or untracked routing itself: linked worktrees often do not contain the untracked `.trellis`, `.agents`, `.codex`, or `.claude` directories that AI tools need before any Trellis hook or skill can run.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Identify the target worktree. Use the user's explicit path if present; otherwise use the current working directory.
|
|
15
|
+
2. Locate a source worktree in the same Git worktree set that contains `.trellis/scripts/worktree_setup.py`.
|
|
16
|
+
3. Run read-only diagnosis first:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python3 <source-worktree>/.trellis/scripts/worktree_setup.py status --target <target-worktree> --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
4. If status is `needs-prepare`, run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 <source-worktree>/.trellis/scripts/worktree_setup.py prepare --target <target-worktree> --json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
5. If status is `blocked` or `error`, stop and report the reason and conflict paths. Do not overwrite or delete user files.
|
|
29
|
+
6. After `ready` or `prepared`, return to the user's original Trellis intent and follow the normal workflow state.
|
|
30
|
+
|
|
31
|
+
## Source Worktree Discovery
|
|
32
|
+
|
|
33
|
+
If `<source-worktree>` is not obvious, inspect Git worktrees:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git -C <target-worktree> worktree list --porcelain
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Choose a listed worktree that contains `.trellis/scripts/worktree_setup.py`. If none exists, report that this project has not been updated with `trellis-worktree` support yet.
|
|
40
|
+
|
|
41
|
+
## Safety Rules
|
|
42
|
+
|
|
43
|
+
- The helper only projects source paths that already exist.
|
|
44
|
+
- MVP projection paths are `.trellis`, `.agents`, `.codex`, and `.claude`.
|
|
45
|
+
- Projection uses symlinks and records `<target-worktree>/.trellis-worktree.json`.
|
|
46
|
+
- Existing non-managed `.codex`, `.claude`, `.agents`, or `.trellis` paths are conflicts.
|
|
47
|
+
- Do not hand-copy platform directories. Copying creates drift between worktrees.
|
|
48
|
+
- Do not treat successful setup as task approval, check approval, or push approval.
|
|
49
|
+
|
|
50
|
+
## Expected Results
|
|
51
|
+
|
|
52
|
+
- `ready`: target worktree already has valid Trellis entry points.
|
|
53
|
+
- `needs-prepare`: target worktree is safe to prepare; run `prepare`.
|
|
54
|
+
- `prepared`: symlinks and manifest were created or repaired.
|
|
55
|
+
- `blocked`: user-owned paths would be overwritten; stop and report `conflicts`.
|
|
56
|
+
- `error`: Git or source-root resolution failed; stop and report `reason`.
|
|
@@ -303,6 +303,7 @@
|
|
|
303
303
|
"severity": "error",
|
|
304
304
|
"target": ".codex/hooks/inject-workflow-state.py",
|
|
305
305
|
"whenOperations": [
|
|
306
|
+
"workflow-state-worktree-root-fallback",
|
|
306
307
|
"workflow-state-codex-session-start-guard",
|
|
307
308
|
"workflow-state-stale-task-status",
|
|
308
309
|
"workflow-state-untracked-helper",
|
|
@@ -312,11 +313,13 @@
|
|
|
312
313
|
"assertion": {
|
|
313
314
|
"type": "required-literal",
|
|
314
315
|
"values": [
|
|
316
|
+
"# BEGIN skill-garden patch workflow-state-worktree-root-fallback v0.6",
|
|
315
317
|
"# BEGIN skill-garden patch workflow-state-codex-session-start-guard v0.6",
|
|
316
318
|
"# BEGIN skill-garden patch workflow-state-stale-task-status v0.6",
|
|
317
319
|
"# BEGIN skill-garden patch workflow-state-untracked-helper v0.6",
|
|
318
320
|
"# BEGIN skill-garden patch workflow-state-breadcrumb-subject v0.6",
|
|
319
321
|
"# BEGIN skill-garden patch workflow-state-main-subject-routing v0.6",
|
|
322
|
+
"_find_trellis_root_from_git",
|
|
320
323
|
"DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD = \"no-trellis\"",
|
|
321
324
|
"\"ZCODE_PROJECT_DIR\": \"zcode\"",
|
|
322
325
|
"Normalize `codex.dispatch_mode` from .trellis/config.yaml to \"auto\" or \"inline\"."
|
package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/patch.json
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
"id": "inject-workflow-state-shared-runtime",
|
|
4
4
|
"purpose": "hook_override",
|
|
5
5
|
"operations": [
|
|
6
|
+
{
|
|
7
|
+
"id": "workflow-state-worktree-root-fallback",
|
|
8
|
+
"operation": "replace",
|
|
9
|
+
"targetPolicy": "each-existing",
|
|
10
|
+
"targets": [
|
|
11
|
+
{ "kind": "file", "path": ".codex/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
12
|
+
{ "kind": "file", "path": ".claude/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
13
|
+
{ "kind": "file", "path": ".gemini/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
14
|
+
{ "kind": "file", "path": ".qoder/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
15
|
+
{ "kind": "file", "path": ".github/copilot/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
16
|
+
{ "kind": "file", "path": ".codebuddy/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
17
|
+
{ "kind": "file", "path": ".factory/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
18
|
+
{ "kind": "file", "path": ".kiro/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
19
|
+
{ "kind": "file", "path": ".trae/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
20
|
+
{ "kind": "file", "path": ".zcode/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" }
|
|
21
|
+
],
|
|
22
|
+
"selector": { "type": "literal", "source": "worktree-root-selector.py" },
|
|
23
|
+
"content": { "source": "worktree-root-content.py" }
|
|
24
|
+
},
|
|
6
25
|
{
|
|
7
26
|
"id": "workflow-state-codex-session-start-guard",
|
|
8
27
|
"operation": "insert",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
def _git_output(start: Path, *args: str) -> Optional[str]:
|
|
2
|
+
"""Run a read-only git command for best-effort Trellis root fallback."""
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
result = subprocess.run(
|
|
7
|
+
["git", "-C", str(start), *args],
|
|
8
|
+
check=False,
|
|
9
|
+
stdout=subprocess.PIPE,
|
|
10
|
+
stderr=subprocess.DEVNULL,
|
|
11
|
+
text=True,
|
|
12
|
+
timeout=2,
|
|
13
|
+
)
|
|
14
|
+
except (OSError, subprocess.SubprocessError):
|
|
15
|
+
return None
|
|
16
|
+
if result.returncode != 0:
|
|
17
|
+
return None
|
|
18
|
+
output = result.stdout.strip()
|
|
19
|
+
return output or None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _resolve_git_common_dir(start: Path) -> Optional[Path]:
|
|
23
|
+
"""Return the current repository's common git dir."""
|
|
24
|
+
output = _git_output(start, "rev-parse", "--path-format=absolute", "--git-common-dir")
|
|
25
|
+
if output is None:
|
|
26
|
+
output = _git_output(start, "rev-parse", "--git-common-dir")
|
|
27
|
+
if output is None:
|
|
28
|
+
return None
|
|
29
|
+
common_dir = Path(output)
|
|
30
|
+
if common_dir.is_absolute():
|
|
31
|
+
return common_dir.resolve()
|
|
32
|
+
top_level = _git_output(start, "rev-parse", "--show-toplevel")
|
|
33
|
+
if top_level is None:
|
|
34
|
+
return None
|
|
35
|
+
return (Path(top_level) / common_dir).resolve()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _find_trellis_root_from_git(start: Path) -> Optional[Path]:
|
|
39
|
+
"""Find a sibling worktree that carries .trellis/ for this Git repository."""
|
|
40
|
+
common_dir = _resolve_git_common_dir(start)
|
|
41
|
+
if common_dir is not None:
|
|
42
|
+
candidate = common_dir.parent
|
|
43
|
+
if (candidate / ".trellis").is_dir():
|
|
44
|
+
return candidate
|
|
45
|
+
|
|
46
|
+
output = _git_output(start, "worktree", "list", "--porcelain")
|
|
47
|
+
if output is None:
|
|
48
|
+
return None
|
|
49
|
+
for line in output.splitlines():
|
|
50
|
+
if not line.startswith("worktree "):
|
|
51
|
+
continue
|
|
52
|
+
candidate = Path(line.split(" ", 1)[1]).expanduser().resolve()
|
|
53
|
+
if (candidate / ".trellis").is_dir():
|
|
54
|
+
return candidate
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def find_trellis_root(start: Path) -> Optional[Path]:
|
|
59
|
+
"""Walk up from start, then fall back to the Git worktree carrying .trellis/.
|
|
60
|
+
|
|
61
|
+
Linked worktrees often do not contain the untracked .trellis/ runtime. The
|
|
62
|
+
hook still needs the main worktree's runtime state so direct-edit cursors can
|
|
63
|
+
be resumed from the linked worktree without copying .trellis/.
|
|
64
|
+
"""
|
|
65
|
+
cur = start.resolve()
|
|
66
|
+
while cur != cur.parent:
|
|
67
|
+
if (cur / ".trellis").is_dir():
|
|
68
|
+
return cur
|
|
69
|
+
cur = cur.parent
|
|
70
|
+
return _find_trellis_root_from_git(start.resolve())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
def find_trellis_root(start: Path) -> Optional[Path]:
|
|
2
|
+
"""Walk up from start to find directory containing .trellis/.
|
|
3
|
+
|
|
4
|
+
Handles CWD drift: subdirectory launches, monorepo packages, etc.
|
|
5
|
+
Returns None if no .trellis/ found (silent no-op).
|
|
6
|
+
"""
|
|
7
|
+
cur = start.resolve()
|
|
8
|
+
while cur != cur.parent:
|
|
9
|
+
if (cur / ".trellis").is_dir():
|
|
10
|
+
return cur
|
|
11
|
+
cur = cur.parent
|
|
12
|
+
return None
|
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import argparse
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
import tempfile
|
|
11
12
|
import uuid
|
|
@@ -82,9 +83,9 @@ def _utc_now() -> str:
|
|
|
82
83
|
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
def
|
|
86
|
-
"""
|
|
87
|
-
current =
|
|
86
|
+
def _find_root_upwards(start: Path) -> Path | None:
|
|
87
|
+
"""从指定路径向上查找带 .trellis 的目录。"""
|
|
88
|
+
current = start.resolve()
|
|
88
89
|
if current.is_file():
|
|
89
90
|
current = current.parent
|
|
90
91
|
while True:
|
|
@@ -95,6 +96,80 @@ def _find_repo_root(start: Path | None = None) -> Path | None:
|
|
|
95
96
|
current = current.parent
|
|
96
97
|
|
|
97
98
|
|
|
99
|
+
def _git_output(start: Path, *args: str) -> str | None:
|
|
100
|
+
"""执行只读 git 命令并返回 stdout,失败时返回 None。"""
|
|
101
|
+
try:
|
|
102
|
+
result = subprocess.run(
|
|
103
|
+
["git", "-C", str(start), *args],
|
|
104
|
+
check=False,
|
|
105
|
+
stdout=subprocess.PIPE,
|
|
106
|
+
stderr=subprocess.DEVNULL,
|
|
107
|
+
text=True,
|
|
108
|
+
timeout=2,
|
|
109
|
+
)
|
|
110
|
+
except (OSError, subprocess.SubprocessError):
|
|
111
|
+
return None
|
|
112
|
+
if result.returncode != 0:
|
|
113
|
+
return None
|
|
114
|
+
output = result.stdout.strip()
|
|
115
|
+
return output or None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _resolve_git_common_dir(start: Path) -> Path | None:
|
|
119
|
+
"""返回当前 Git 仓库的 common dir,兼容旧 git 的相对路径输出。"""
|
|
120
|
+
output = _git_output(start, "rev-parse", "--path-format=absolute", "--git-common-dir")
|
|
121
|
+
if output is None:
|
|
122
|
+
output = _git_output(start, "rev-parse", "--git-common-dir")
|
|
123
|
+
if output is None:
|
|
124
|
+
return None
|
|
125
|
+
common_dir = Path(output)
|
|
126
|
+
if common_dir.is_absolute():
|
|
127
|
+
return common_dir.resolve()
|
|
128
|
+
top_level = _git_output(start, "rev-parse", "--show-toplevel")
|
|
129
|
+
if top_level is None:
|
|
130
|
+
return None
|
|
131
|
+
return (Path(top_level) / common_dir).resolve()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _find_root_from_git_worktrees(start: Path) -> Path | None:
|
|
135
|
+
"""从同一个 Git worktree 集合里寻找承载 .trellis 的主项目根。"""
|
|
136
|
+
current = start.resolve()
|
|
137
|
+
if current.is_file():
|
|
138
|
+
current = current.parent
|
|
139
|
+
|
|
140
|
+
common_dir = _resolve_git_common_dir(current)
|
|
141
|
+
if common_dir is not None:
|
|
142
|
+
candidate = common_dir.parent
|
|
143
|
+
if (candidate / ".trellis").is_dir():
|
|
144
|
+
return candidate
|
|
145
|
+
|
|
146
|
+
output = _git_output(current, "worktree", "list", "--porcelain")
|
|
147
|
+
if output is None:
|
|
148
|
+
return None
|
|
149
|
+
for line in output.splitlines():
|
|
150
|
+
if not line.startswith("worktree "):
|
|
151
|
+
continue
|
|
152
|
+
candidate = Path(line.split(" ", 1)[1]).expanduser().resolve()
|
|
153
|
+
if (candidate / ".trellis").is_dir():
|
|
154
|
+
return candidate
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _find_repo_root(start: Path | None = None) -> Path | None:
|
|
159
|
+
"""查找 Trellis 项目根目录,兼容没有 .trellis 的 linked worktree。"""
|
|
160
|
+
current = (start or Path.cwd()).resolve()
|
|
161
|
+
root = _find_root_upwards(current)
|
|
162
|
+
if root is not None:
|
|
163
|
+
return root
|
|
164
|
+
root = _find_root_from_git_worktrees(current)
|
|
165
|
+
if root is not None:
|
|
166
|
+
return root
|
|
167
|
+
for parent in Path(__file__).resolve().parents:
|
|
168
|
+
if (parent / ".trellis").is_dir():
|
|
169
|
+
return parent
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
98
173
|
def _session_path(repo_root: Path, context_key: str) -> Path:
|
|
99
174
|
"""返回指定 session runtime 文件路径。"""
|
|
100
175
|
return repo_root / ".trellis/.runtime/sessions" / f"{context_key}.json"
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""准备 linked worktree 中的 Trellis 入口投影。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SCHEMA_VERSION = 1
|
|
18
|
+
MANIFEST_NAME = ".trellis-worktree.json"
|
|
19
|
+
ENTRY_PATHS = (".trellis", ".agents", ".codex", ".claude")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorktreeSetupError(Exception):
|
|
23
|
+
"""携带稳定 reason code 的 worktree 准备错误。"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, reason: str, message: str, **details: Any) -> None:
|
|
26
|
+
"""初始化结构化准备错误。
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
reason: 稳定机器错误码。
|
|
30
|
+
message: 中文诊断说明。
|
|
31
|
+
**details: 与错误相关的状态详情。
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.reason = reason
|
|
35
|
+
self.details = details
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _utc_now() -> str:
|
|
39
|
+
"""返回秒级 UTC ISO-8601 时间。"""
|
|
40
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _path_text(path: Path) -> str:
|
|
44
|
+
"""返回稳定的绝对路径字符串,但不跟随最终 symlink。"""
|
|
45
|
+
return os.path.abspath(os.fspath(path.expanduser()))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _git_output(start: Path, *args: str) -> str | None:
|
|
49
|
+
"""执行只读 git 命令并返回 stdout,失败时返回 None。"""
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["git", "-C", str(start), *args],
|
|
53
|
+
check=False,
|
|
54
|
+
stdout=subprocess.PIPE,
|
|
55
|
+
stderr=subprocess.DEVNULL,
|
|
56
|
+
text=True,
|
|
57
|
+
timeout=2,
|
|
58
|
+
)
|
|
59
|
+
except (OSError, subprocess.SubprocessError):
|
|
60
|
+
return None
|
|
61
|
+
if result.returncode != 0:
|
|
62
|
+
return None
|
|
63
|
+
output = result.stdout.strip()
|
|
64
|
+
return output or None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _resolve_start(path_text: str | None) -> Path:
|
|
68
|
+
"""解析用户传入的 target,允许传入 worktree 子目录。"""
|
|
69
|
+
path = Path(path_text or os.getcwd()).expanduser()
|
|
70
|
+
if not path.exists():
|
|
71
|
+
raise WorktreeSetupError("target-missing", "目标路径不存在", target=str(path))
|
|
72
|
+
resolved = path.resolve()
|
|
73
|
+
return resolved.parent if resolved.is_file() else resolved
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _git_toplevel(start: Path) -> Path | None:
|
|
77
|
+
"""返回当前 Git worktree 根目录。"""
|
|
78
|
+
output = _git_output(start, "rev-parse", "--path-format=absolute", "--show-toplevel")
|
|
79
|
+
if output is None:
|
|
80
|
+
output = _git_output(start, "rev-parse", "--show-toplevel")
|
|
81
|
+
return Path(output).expanduser().resolve() if output else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _git_common_dir(start: Path) -> Path | None:
|
|
85
|
+
"""返回当前 Git 仓库 common dir,兼容旧 git 的相对路径输出。"""
|
|
86
|
+
output = _git_output(start, "rev-parse", "--path-format=absolute", "--git-common-dir")
|
|
87
|
+
if output is None:
|
|
88
|
+
output = _git_output(start, "rev-parse", "--git-common-dir")
|
|
89
|
+
if output is None:
|
|
90
|
+
return None
|
|
91
|
+
common_dir = Path(output)
|
|
92
|
+
if common_dir.is_absolute():
|
|
93
|
+
return common_dir.resolve()
|
|
94
|
+
top_level = _git_toplevel(start)
|
|
95
|
+
if top_level is None:
|
|
96
|
+
return None
|
|
97
|
+
return (top_level / common_dir).resolve()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _manifest_path(target_root: Path) -> Path:
|
|
101
|
+
"""返回目标 worktree 的投影 manifest 路径。"""
|
|
102
|
+
return target_root / MANIFEST_NAME
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _read_manifest(target_root: Path) -> tuple[str, dict[str, Any] | None]:
|
|
106
|
+
"""读取 manifest,并保留缺失和损坏差异。"""
|
|
107
|
+
path = _manifest_path(target_root)
|
|
108
|
+
try:
|
|
109
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
110
|
+
except FileNotFoundError:
|
|
111
|
+
return "missing", None
|
|
112
|
+
except (json.JSONDecodeError, OSError):
|
|
113
|
+
return "invalid", None
|
|
114
|
+
if not isinstance(data, dict):
|
|
115
|
+
return "invalid", None
|
|
116
|
+
return "ok", data
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _manifest_source(target_root: Path, manifest: dict[str, Any] | None) -> Path | None:
|
|
120
|
+
"""从有效 manifest 中读取 sourceRoot。"""
|
|
121
|
+
if not isinstance(manifest, dict) or manifest.get("schemaVersion") != SCHEMA_VERSION:
|
|
122
|
+
return None
|
|
123
|
+
target_text = manifest.get("targetRoot")
|
|
124
|
+
source_text = manifest.get("sourceRoot")
|
|
125
|
+
if not isinstance(target_text, str) or not isinstance(source_text, str):
|
|
126
|
+
return None
|
|
127
|
+
if Path(target_text).expanduser().resolve() != target_root:
|
|
128
|
+
return None
|
|
129
|
+
source_root = Path(source_text).expanduser().resolve()
|
|
130
|
+
return source_root if (source_root / ".trellis").is_dir() else None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _manifest_paths(manifest: dict[str, Any] | None) -> set[str]:
|
|
134
|
+
"""返回 manifest 记录的受管相对路径集合。"""
|
|
135
|
+
links = manifest.get("links") if isinstance(manifest, dict) else None
|
|
136
|
+
if not isinstance(links, list):
|
|
137
|
+
return set()
|
|
138
|
+
result: set[str] = set()
|
|
139
|
+
for item in links:
|
|
140
|
+
if not isinstance(item, dict):
|
|
141
|
+
continue
|
|
142
|
+
path = item.get("path")
|
|
143
|
+
if isinstance(path, str) and path in ENTRY_PATHS:
|
|
144
|
+
result.add(path)
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _source_from_trellis_symlink(target_root: Path) -> Path | None:
|
|
149
|
+
"""从目标 .trellis symlink 反推出主 worktree 根。"""
|
|
150
|
+
trellis_path = target_root / ".trellis"
|
|
151
|
+
if not trellis_path.is_symlink():
|
|
152
|
+
return None
|
|
153
|
+
source_path = trellis_path.resolve(strict=False)
|
|
154
|
+
if source_path.name != ".trellis":
|
|
155
|
+
return None
|
|
156
|
+
source_root = source_path.parent
|
|
157
|
+
return source_root if source_path.is_dir() else None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _source_from_git(target_root: Path) -> Path | None:
|
|
161
|
+
"""从同一个 Git worktree 集合中寻找带 .trellis 的主 worktree。"""
|
|
162
|
+
common_dir = _git_common_dir(target_root)
|
|
163
|
+
if common_dir is not None:
|
|
164
|
+
candidate = common_dir.parent
|
|
165
|
+
if candidate != target_root and (candidate / ".trellis").is_dir():
|
|
166
|
+
return candidate
|
|
167
|
+
|
|
168
|
+
output = _git_output(target_root, "worktree", "list", "--porcelain")
|
|
169
|
+
if output is None:
|
|
170
|
+
return None
|
|
171
|
+
fallback: Path | None = None
|
|
172
|
+
for line in output.splitlines():
|
|
173
|
+
if not line.startswith("worktree "):
|
|
174
|
+
continue
|
|
175
|
+
candidate = Path(line.split(" ", 1)[1]).expanduser().resolve()
|
|
176
|
+
if not (candidate / ".trellis").is_dir():
|
|
177
|
+
continue
|
|
178
|
+
if candidate != target_root:
|
|
179
|
+
return candidate
|
|
180
|
+
fallback = candidate
|
|
181
|
+
return fallback
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _resolve_source_root(
|
|
185
|
+
target_root: Path,
|
|
186
|
+
manifest: dict[str, Any] | None,
|
|
187
|
+
) -> tuple[Path, str]:
|
|
188
|
+
"""解析承载 .trellis 的源 worktree。"""
|
|
189
|
+
source = _manifest_source(target_root, manifest)
|
|
190
|
+
if source is not None:
|
|
191
|
+
return source, "manifest"
|
|
192
|
+
|
|
193
|
+
source = _source_from_trellis_symlink(target_root)
|
|
194
|
+
if source is not None:
|
|
195
|
+
return source, "trellis-symlink"
|
|
196
|
+
|
|
197
|
+
source = _source_from_git(target_root)
|
|
198
|
+
if source is not None:
|
|
199
|
+
return source, "git-worktree"
|
|
200
|
+
|
|
201
|
+
if (target_root / ".trellis").is_dir():
|
|
202
|
+
return target_root, "target-root"
|
|
203
|
+
|
|
204
|
+
raise WorktreeSetupError(
|
|
205
|
+
"source-not-found",
|
|
206
|
+
"无法在当前 Git worktree 集合中找到承载 .trellis 的主 worktree",
|
|
207
|
+
targetRoot=_path_text(target_root),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _same_symlink_target(link: Path, source: Path) -> bool:
|
|
212
|
+
"""判断 symlink 是否已经指向期望源路径。"""
|
|
213
|
+
try:
|
|
214
|
+
raw_target = Path(os.readlink(link))
|
|
215
|
+
except OSError:
|
|
216
|
+
return False
|
|
217
|
+
actual = raw_target if raw_target.is_absolute() else (link.parent / raw_target)
|
|
218
|
+
return actual.resolve(strict=False) == source.resolve(strict=False)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _classify_link(
|
|
222
|
+
source_root: Path,
|
|
223
|
+
target_root: Path,
|
|
224
|
+
rel_path: str,
|
|
225
|
+
managed_paths: set[str],
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""判断单个入口路径的投影状态。"""
|
|
228
|
+
source = source_root / rel_path
|
|
229
|
+
target = target_root / rel_path
|
|
230
|
+
item = {
|
|
231
|
+
"path": rel_path,
|
|
232
|
+
"source": _path_text(source),
|
|
233
|
+
"target": _path_text(target),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if not source.exists() and not source.is_symlink():
|
|
237
|
+
return {**item, "state": "source-missing"}
|
|
238
|
+
if source_root == target_root:
|
|
239
|
+
return {**item, "state": "ready"}
|
|
240
|
+
if target.is_symlink():
|
|
241
|
+
if _same_symlink_target(target, source):
|
|
242
|
+
return {**item, "state": "ready"}
|
|
243
|
+
if rel_path in managed_paths:
|
|
244
|
+
return {**item, "state": "repair"}
|
|
245
|
+
return {**item, "state": "conflict", "reason": "symlink-target-mismatch"}
|
|
246
|
+
if target.exists():
|
|
247
|
+
return {**item, "state": "conflict", "reason": "target-exists"}
|
|
248
|
+
return {**item, "state": "create"}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _analyze(target_arg: str | None) -> dict[str, Any]:
|
|
252
|
+
"""生成 worktree 准备计划,不写盘。"""
|
|
253
|
+
start = _resolve_start(target_arg)
|
|
254
|
+
target_root = _git_toplevel(start)
|
|
255
|
+
if target_root is None:
|
|
256
|
+
raise WorktreeSetupError("not-git-worktree", "目标路径不在 Git worktree 中", target=str(start))
|
|
257
|
+
|
|
258
|
+
manifest_status, manifest = _read_manifest(target_root)
|
|
259
|
+
source_root, source = _resolve_source_root(target_root, manifest)
|
|
260
|
+
managed_paths = _manifest_paths(manifest)
|
|
261
|
+
links = [
|
|
262
|
+
_classify_link(source_root, target_root, rel_path, managed_paths)
|
|
263
|
+
for rel_path in ENTRY_PATHS
|
|
264
|
+
]
|
|
265
|
+
actions = [link for link in links if link["state"] in {"create", "repair"}]
|
|
266
|
+
conflicts = [link for link in links if link["state"] == "conflict"]
|
|
267
|
+
missing_sources = [link for link in links if link["state"] == "source-missing"]
|
|
268
|
+
|
|
269
|
+
status = "ready"
|
|
270
|
+
if conflicts:
|
|
271
|
+
status = "blocked"
|
|
272
|
+
elif actions:
|
|
273
|
+
status = "needs-prepare"
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
"status": status,
|
|
277
|
+
"targetRoot": _path_text(target_root),
|
|
278
|
+
"sourceRoot": _path_text(source_root),
|
|
279
|
+
"source": source,
|
|
280
|
+
"manifest": {
|
|
281
|
+
"path": _path_text(_manifest_path(target_root)),
|
|
282
|
+
"status": manifest_status,
|
|
283
|
+
},
|
|
284
|
+
"links": links,
|
|
285
|
+
"actions": actions,
|
|
286
|
+
"conflicts": conflicts,
|
|
287
|
+
"missingSources": missing_sources,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _desired_manifest(plan: dict[str, Any]) -> dict[str, Any]:
|
|
292
|
+
"""构造当前投影的 manifest 内容。"""
|
|
293
|
+
links = [
|
|
294
|
+
{
|
|
295
|
+
"path": link["path"],
|
|
296
|
+
"source": link["source"],
|
|
297
|
+
"target": link["target"],
|
|
298
|
+
}
|
|
299
|
+
for link in plan["links"]
|
|
300
|
+
if link["state"] != "source-missing"
|
|
301
|
+
]
|
|
302
|
+
return {
|
|
303
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
304
|
+
"sourceRoot": plan["sourceRoot"],
|
|
305
|
+
"targetRoot": plan["targetRoot"],
|
|
306
|
+
"links": links,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _manifest_matches(path: Path, desired: dict[str, Any]) -> bool:
|
|
311
|
+
"""判断现有 manifest 是否已经与期望投影一致,忽略 updatedAt。"""
|
|
312
|
+
try:
|
|
313
|
+
current = json.loads(path.read_text(encoding="utf-8"))
|
|
314
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
315
|
+
return False
|
|
316
|
+
if not isinstance(current, dict):
|
|
317
|
+
return False
|
|
318
|
+
comparable = {key: current.get(key) for key in ("schemaVersion", "sourceRoot", "targetRoot", "links")}
|
|
319
|
+
return comparable == desired
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _write_manifest(path: Path, desired: dict[str, Any]) -> None:
|
|
323
|
+
"""原子写入 worktree 投影 manifest。"""
|
|
324
|
+
data = {**desired, "updatedAt": _utc_now()}
|
|
325
|
+
descriptor, temp_name = tempfile.mkstemp(
|
|
326
|
+
prefix=f".{path.name}.",
|
|
327
|
+
suffix=".tmp",
|
|
328
|
+
dir=path.parent,
|
|
329
|
+
)
|
|
330
|
+
temp_path = Path(temp_name)
|
|
331
|
+
try:
|
|
332
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
333
|
+
handle.write(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
|
334
|
+
handle.flush()
|
|
335
|
+
os.fsync(handle.fileno())
|
|
336
|
+
os.replace(temp_path, path)
|
|
337
|
+
except Exception:
|
|
338
|
+
try:
|
|
339
|
+
temp_path.unlink()
|
|
340
|
+
except FileNotFoundError:
|
|
341
|
+
pass
|
|
342
|
+
raise
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _apply_plan(plan: dict[str, Any]) -> dict[str, Any]:
|
|
346
|
+
"""执行准备计划并返回写盘结果。"""
|
|
347
|
+
if plan["status"] == "blocked":
|
|
348
|
+
raise WorktreeSetupError(
|
|
349
|
+
"projection-conflict",
|
|
350
|
+
"目标 worktree 中存在非 trellis-worktree 管理的入口路径,已拒绝覆盖",
|
|
351
|
+
conflicts=plan["conflicts"],
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
changed_links: list[str] = []
|
|
355
|
+
for action in plan["actions"]:
|
|
356
|
+
source = Path(action["source"])
|
|
357
|
+
target = Path(action["target"])
|
|
358
|
+
if action["state"] == "repair":
|
|
359
|
+
target.unlink()
|
|
360
|
+
os.symlink(source, target, target_is_directory=source.is_dir())
|
|
361
|
+
changed_links.append(action["path"])
|
|
362
|
+
|
|
363
|
+
manifest_written = False
|
|
364
|
+
if plan["sourceRoot"] != plan["targetRoot"]:
|
|
365
|
+
manifest_path = Path(plan["manifest"]["path"])
|
|
366
|
+
desired = _desired_manifest(plan)
|
|
367
|
+
if changed_links or not _manifest_matches(manifest_path, desired):
|
|
368
|
+
_write_manifest(manifest_path, desired)
|
|
369
|
+
manifest_written = True
|
|
370
|
+
|
|
371
|
+
status = "prepared" if changed_links or manifest_written else "ready"
|
|
372
|
+
return {
|
|
373
|
+
**plan,
|
|
374
|
+
"status": status,
|
|
375
|
+
"changed": bool(changed_links or manifest_written),
|
|
376
|
+
"changedLinks": changed_links,
|
|
377
|
+
"manifestWritten": manifest_written,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _emit(payload: dict[str, Any], *, compact: bool) -> None:
|
|
382
|
+
"""向 stdout 输出 JSON。"""
|
|
383
|
+
print(json.dumps(payload, ensure_ascii=False, sort_keys=True if compact else False, indent=None if compact else 2))
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
387
|
+
"""构造 worktree setup CLI parser。"""
|
|
388
|
+
parser = argparse.ArgumentParser(description="Trellis linked worktree setup helper")
|
|
389
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
390
|
+
|
|
391
|
+
for command in ("status", "prepare"):
|
|
392
|
+
command_parser = subparsers.add_parser(command)
|
|
393
|
+
command_parser.add_argument("--target", help="linked worktree path; defaults to cwd")
|
|
394
|
+
command_parser.add_argument("--json", action="store_true", help="emit compact stable JSON")
|
|
395
|
+
return parser
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def main(argv: list[str] | None = None) -> int:
|
|
399
|
+
"""执行 worktree setup CLI。"""
|
|
400
|
+
args = build_parser().parse_args(argv)
|
|
401
|
+
compact = bool(getattr(args, "json", False))
|
|
402
|
+
try:
|
|
403
|
+
plan = _analyze(args.target)
|
|
404
|
+
payload = _apply_plan(plan) if args.command == "prepare" else plan
|
|
405
|
+
_emit(payload, compact=compact)
|
|
406
|
+
return 0
|
|
407
|
+
except (WorktreeSetupError, OSError) as error:
|
|
408
|
+
if isinstance(error, WorktreeSetupError):
|
|
409
|
+
payload = {
|
|
410
|
+
"status": "error",
|
|
411
|
+
"reason": error.reason,
|
|
412
|
+
"message": str(error),
|
|
413
|
+
**error.details,
|
|
414
|
+
}
|
|
415
|
+
else:
|
|
416
|
+
payload = {"status": "error", "reason": "worktree-setup-failed", "message": str(error)}
|
|
417
|
+
_emit(payload, compact=compact)
|
|
418
|
+
return 1
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
if __name__ == "__main__":
|
|
422
|
+
sys.exit(main())
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-08-
|
|
2
|
+
"syncedAt": "2026-08-04T10:14:37.206Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden",
|
|
4
|
-
"sourceCommit": "
|
|
4
|
+
"sourceCommit": "5dbd85563bd521a93fcde4fe8b3e00993d75e7b6",
|
|
5
5
|
"common": {
|
|
6
6
|
"codexSkills": [
|
|
7
7
|
"aliyun-sls-query",
|
|
@@ -112,7 +112,8 @@
|
|
|
112
112
|
"trellis-run-full-chain",
|
|
113
113
|
"trellis-task-brief",
|
|
114
114
|
"trellis-verify-task",
|
|
115
|
-
"trellis-visualize"
|
|
115
|
+
"trellis-visualize",
|
|
116
|
+
"trellis-worktree"
|
|
116
117
|
],
|
|
117
118
|
"agentsSkills": [
|
|
118
119
|
"trellis-auto-loop",
|
|
@@ -127,7 +128,8 @@
|
|
|
127
128
|
"trellis-run-full-chain",
|
|
128
129
|
"trellis-task-brief",
|
|
129
130
|
"trellis-verify-task",
|
|
130
|
-
"trellis-visualize"
|
|
131
|
+
"trellis-visualize",
|
|
132
|
+
"trellis-worktree"
|
|
131
133
|
],
|
|
132
134
|
"commands": [],
|
|
133
135
|
"overrides": [],
|
|
@@ -168,6 +170,8 @@
|
|
|
168
170
|
"hooks/inject-workflow-state/shared-runtime/stale-task-selector.py",
|
|
169
171
|
"hooks/inject-workflow-state/shared-runtime/untracked-helper-content.py",
|
|
170
172
|
"hooks/inject-workflow-state/shared-runtime/untracked-helper-selector.py",
|
|
173
|
+
"hooks/inject-workflow-state/shared-runtime/worktree-root-content.py",
|
|
174
|
+
"hooks/inject-workflow-state/shared-runtime/worktree-root-selector.py",
|
|
171
175
|
"hooks/session-start/pre-check-hold/claude-content.py",
|
|
172
176
|
"hooks/session-start/pre-check-hold/claude-selector.py",
|
|
173
177
|
"hooks/session-start/pre-check-hold/codex-content.py",
|
|
@@ -432,7 +436,8 @@
|
|
|
432
436
|
"spec_router.py",
|
|
433
437
|
"task_intent.py",
|
|
434
438
|
"task_progress.py",
|
|
435
|
-
"untracked_flow.py"
|
|
439
|
+
"untracked_flow.py",
|
|
440
|
+
"worktree_setup.py"
|
|
436
441
|
]
|
|
437
442
|
}
|
|
438
443
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: humanize-writing
|
|
3
|
-
description: "中文文本润色与去 AI 腔改写。用于用户要求润色中文、去掉 AI
|
|
3
|
+
description: "中文文本润色与去 AI 腔改写。用于用户要求润色中文、去掉 AI 味、改得像人写的、自然化表达、优化中文语气、压缩空话、调整人味、保留原意但让文字更顺时触发。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 中文写作人味化:移除 AI 写作痕迹
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
你是中文写作编辑。用户给出中文文本时,目标不是炫技改写,也不是规避检测器,而是在保留原意、信息量和必要结构的前提下,识别并移除常见 AI 写作痕迹,让文字恢复真实作者会采用的表达方式。
|
|
9
9
|
|
|
10
10
|
本技能的重点是中文文本。遇到英文术语、专有名词、代码、引用、标题或用户明确要求保留的表达时,不要为了“中文化”强行改掉。
|
|
11
11
|
|
|
@@ -13,17 +13,98 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
13
13
|
|
|
14
14
|
处理需要自然化的文本时:
|
|
15
15
|
|
|
16
|
-
1.
|
|
17
|
-
2.
|
|
18
|
-
3.
|
|
19
|
-
4.
|
|
16
|
+
1. **先判断场景**:确认文本是技术文档、PRD、说明、商务沟通、公告、评论、博客、社交文案还是个人表达。
|
|
17
|
+
2. **选择去味强度**:默认中度处理;正式、高风险文本只做轻度清晰化;用户明确要求“狠一点”“像真人”时才做强度改写。
|
|
18
|
+
3. **识别 AI 痕迹**:按下方规则扫描空话、套话、机械结构、虚假深度、过度客气、不自然标点和均匀节奏。
|
|
19
|
+
4. **改写,不是删除**:把 AI 腔换成自然中文,同时覆盖原文的全部信息。原文有五段,改写后也应大致保留五段的信息量和节奏。
|
|
20
|
+
5. **保留原意**:不新增事实、结论、数据、承诺、情绪或立场。
|
|
21
|
+
6. **匹配语气**:按文本用途选择正式、日常、技术、商务、评论、笔记等语气。只有当内容和作者声音需要时,才加入个性。
|
|
20
22
|
|
|
21
23
|
处理流程和输出要求见文末“处理流程与输出”。
|
|
22
24
|
|
|
25
|
+
## 判断顺序
|
|
26
|
+
|
|
27
|
+
去 AI 味时按这个顺序决策:
|
|
28
|
+
|
|
29
|
+
1. **先保真**:人物、对象、时间、数字、因果、限制条件、风险提示和结论不能变。
|
|
30
|
+
2. **再定场景**:不同文本允许的改写自由度不同。技术说明要清楚,个人表达才需要更明显的人声。
|
|
31
|
+
3. **再定声线**:有样本时匹配作者 voice;没有样本时只调整当前文本 tone,不凭空创造作者人格。
|
|
32
|
+
4. **最后动句子**:替换套话、拆开机械结构、补动作主体、调整节奏。
|
|
33
|
+
|
|
34
|
+
不要一上来就替换禁用词。AI 味常来自结构和信息组织,不只是某几个词。
|
|
35
|
+
|
|
36
|
+
## 文本场景与去味强度
|
|
37
|
+
|
|
38
|
+
### 场景模式
|
|
39
|
+
|
|
40
|
+
- **技术文档 / PRD / 说明 / release note**:重点是动作、边界、条件和结果。保留术语、字段名、路径、版本号、接口名和命令。可以减少官话和被动句,但不要加情绪、段子或第一人称。
|
|
41
|
+
- **商务沟通 / 工作汇报 / 周报**:先结论,再事实,再下一步。删掉拔高意义、套话式总结和过度客气,保留必要的克制和礼貌。
|
|
42
|
+
- **公告 / 法务 / 合同 / 政策文本**:只做清晰化和歧义消除。不要为了“像人”改变义务、权限、承诺或责任主体。
|
|
43
|
+
- **博客 / 评论 / 个人总结**:允许更明显的判断、偏好、犹豫和句长变化。可以保留一点不整齐,但不能编造经历或观点。
|
|
44
|
+
- **社交文案 / 轻内容**:可以更短、更直接、更有口语节奏。不要默认改成营销号、小红书或标题党口吻。
|
|
45
|
+
|
|
46
|
+
### 去味强度
|
|
47
|
+
|
|
48
|
+
- **轻度**:去掉聊天机器人寒暄、重复铺垫、空泛结尾、明显套话和不自然标点。适合正式、技术、高风险文本。
|
|
49
|
+
- **中度(默认)**:重排句子,补清主语,改掉机械排比和抽象名词,把泛泛表达换成具体动作或事实。适合大多数文本。
|
|
50
|
+
- **强度**:重建节奏和表达方式,让文字更像真人现场写出的版本。只在用户明确要求、或文本本身属于个人表达/社交/博客时使用。
|
|
51
|
+
|
|
52
|
+
如果用户没有指定强度,先按场景选择。不要为了“人味”牺牲准确性。
|
|
53
|
+
|
|
54
|
+
## 结构诊断
|
|
55
|
+
|
|
56
|
+
AI 味通常不是单个词造成的,而是多个结构信号叠加:
|
|
57
|
+
|
|
58
|
+
- **句式过匀**:每句长度接近,段落节奏像模板生成。
|
|
59
|
+
- **段落过整**:每段都按“背景、价值、意义、未来”收束。
|
|
60
|
+
- **动作无主**:大量“被保留、已完成、得到提升、持续推进”,看不出谁做了什么。
|
|
61
|
+
- **抽象替代事实**:用“能力、体系、生态、价值、底层逻辑”代替真实动作、对象和结果。
|
|
62
|
+
- **安全情绪**:永远积极、稳妥、正确,没有实际判断、取舍或边界。
|
|
63
|
+
- **伪深度**:用“真正的问题在于、从本质上看、不是……而是……”制造洞察感。
|
|
64
|
+
- **假完整**:硬凑三点、挑战与展望、泛泛未来,让文本像一份自动生成的大纲。
|
|
65
|
+
|
|
66
|
+
诊断时看组合,不要因为一个破折号、一个过渡词或正式语气就判定为 AI 腔。
|
|
67
|
+
|
|
68
|
+
## 编辑动作库
|
|
69
|
+
|
|
70
|
+
优先使用这些动作去 AI 味:
|
|
71
|
+
|
|
72
|
+
- **抽象名词换成具体动作**:把“能力建设得到提升”改成“团队把审批入口从三个合成一个”。
|
|
73
|
+
- **被动句补动作主体**:把“结果会被保留”改成“系统会保留结果”,或“你可以在记录页查看结果”。
|
|
74
|
+
- **形容词换成证据**:把“体验显著提升”改成“提交步骤从 5 步减少到 3 步”。
|
|
75
|
+
- **三点式改成真实结构**:有顺序就写顺序,有主次就写主次,没有第三点就不要凑。
|
|
76
|
+
- **泛泛结尾改成事实或直接结束**:没有下一步时,不要补“未来可期”。
|
|
77
|
+
- **金句改成普通判断**:把“效率是组织的语言”改成“团队如果只看效率,容易忽略用户怎么操作”。
|
|
78
|
+
- **同义词轮换改回一致称呼**:同一个对象保持同一个名称,除非上下文确实需要区分。
|
|
79
|
+
- **长句拆成判断和解释**:先把结论说清楚,再补原因或例子。
|
|
80
|
+
- **短碎句合并掉戏剧感**:一个短句可以强调,连续短句要收回自然语流。
|
|
81
|
+
|
|
82
|
+
改写时先问:这句话到底在说谁、做了什么、为什么重要、读者需要知道什么。答案清楚后再组织语言。
|
|
83
|
+
|
|
84
|
+
## 保护清单
|
|
85
|
+
|
|
86
|
+
以下内容默认不改,除非用户明确要求:
|
|
87
|
+
|
|
88
|
+
- 数字、金额、日期、版本号、百分比、范围和计量单位。
|
|
89
|
+
- 人名、机构名、地名、产品名、函数名、字段名、接口名、路径、命令、代码片段。
|
|
90
|
+
- 引用、标题、法律条款、合同措辞、合规声明、免责声明。
|
|
91
|
+
- 原文中的承诺、限制、风险、责任主体、因果关系和结论强度。
|
|
92
|
+
- 用户特意保留的语气、格式、称呼、术语或品牌表达。
|
|
93
|
+
|
|
94
|
+
如果为了自然化必须改动这些内容,先在回复中说明风险并保守处理。
|
|
95
|
+
|
|
96
|
+
## AI 检测器边界
|
|
97
|
+
|
|
98
|
+
不要承诺“通过 AI 检测”或“规避检测器”。检测器会误判,也容易受语言复杂度、母语背景、文本类型和改写工具影响。
|
|
99
|
+
|
|
100
|
+
本技能的目标是让文本更真实、更具体、更符合作者和场景,不是随机替换词、故意增加罕见词、制造错别字或堆复杂句。为了检测分数牺牲清楚和准确,属于失败。
|
|
101
|
+
|
|
23
102
|
## 语气校准(可选)
|
|
24
103
|
|
|
25
104
|
如果用户提供了自己的写作样本,先分析样本,再改写目标文本。
|
|
26
105
|
|
|
106
|
+
把 **voice** 和 **tone** 分开看:voice 是作者长期稳定的表达习惯,tone 是这一段文字面对具体读者和场景时的语气。样本只能帮助匹配 voice,不能覆盖当前文本的场景要求。
|
|
107
|
+
|
|
27
108
|
1. **先读样本**,记录:
|
|
28
109
|
- 句长习惯:短句多,长句多,还是长短混用。
|
|
29
110
|
- 用词层级:口语、书面、学术、商务,或介于两者之间。
|
|
@@ -72,6 +153,38 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
72
153
|
|
|
73
154
|
> 我对这个实验其实有点拿不准。三百万行代码,还是在人睡觉的时候跑出来的。有人觉得这就是未来,也有人说这根本不能算真正的开发。结论可能没那么戏剧化,但我一直在想,那些代理整晚跑任务的画面到底意味着什么。
|
|
74
155
|
|
|
156
|
+
## 技术和文档文本
|
|
157
|
+
|
|
158
|
+
技术文档、PRD、issue、release note、注释和操作说明里的 AI 味,通常不是“没人格”,而是动作主体不清、边界含糊、句子像汇报稿。处理这类文本时,优先让读者知道怎么判断、怎么操作、风险在哪里。
|
|
159
|
+
|
|
160
|
+
### 技术文本常见问题
|
|
161
|
+
|
|
162
|
+
- 用“优化、增强、完善、支持”概括所有改动,却不说具体行为。
|
|
163
|
+
- 只从这次 diff 出发写“新增了、替代了”,没有描述当前事实。
|
|
164
|
+
- 大量“可实现、可支持、可提升”,读者看不出现在到底能不能用。
|
|
165
|
+
- 把限制条件放到句尾,导致读者先误解再修正。
|
|
166
|
+
- 用“无需猜测、自动处理、正常处理”遮住实际规则。
|
|
167
|
+
|
|
168
|
+
### 技术文本改写原则
|
|
169
|
+
|
|
170
|
+
- **先写当前事实**:文档要脱离最近一次提交也能读懂。
|
|
171
|
+
- **条件放前面**:先说“当 X 时”,再说系统或用户该做什么。
|
|
172
|
+
- **动作主体明确**:区分用户、系统、脚本、服务端、客户端、CI。
|
|
173
|
+
- **保留术语**:不要把准确的技术名词改成泛泛口语。
|
|
174
|
+
- **不要强行人设**:技术文本可以自然,但不需要像随笔。
|
|
175
|
+
|
|
176
|
+
**改前:**
|
|
177
|
+
> 本次能力升级进一步完善了 worktree 场景下的 untracked 处理流程,确保相关文件能够被正常识别,无需用户额外感知。
|
|
178
|
+
|
|
179
|
+
**改后:**
|
|
180
|
+
> 在 worktree 中运行时,untracked 检查会从当前工作目录读取 `.claude`、`.codex` 和 `.trellis` 相关文件。这样用户在 worktree 里改 skill 或配置时,流程能把这些文件纳入检查。
|
|
181
|
+
|
|
182
|
+
**改前:**
|
|
183
|
+
> 新增该参数是为了提升跨目录目标的兼容性。
|
|
184
|
+
|
|
185
|
+
**改后:**
|
|
186
|
+
> `--target` 可以指向当前仓库外的目录。脚本会在目标目录查找 Trellis 配置,而不是默认使用启动命令时所在的目录。
|
|
187
|
+
|
|
75
188
|
## 内容模式
|
|
76
189
|
|
|
77
190
|
### 1. 过度拔高意义、历史地位和大趋势
|
|
@@ -529,10 +642,13 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
529
642
|
|
|
530
643
|
## 处理流程与输出
|
|
531
644
|
|
|
532
|
-
1.
|
|
533
|
-
2.
|
|
534
|
-
3.
|
|
535
|
-
4.
|
|
645
|
+
1. **判定场景**:确认文本类型、读者、风险等级,以及是否有作者样本。
|
|
646
|
+
2. **选择强度**:轻度、中度或强度。用户没有指定时,按场景自动选择。
|
|
647
|
+
3. **列保护项**:在内部记住不能改的数字、专名、术语、承诺、限制、引用和格式。
|
|
648
|
+
4. **做结构诊断**:找出最影响人味的 2 到 4 个问题,优先处理结构问题,再处理词语问题。
|
|
649
|
+
5. **写草稿改写**:保留信息量和必要结构,去掉空话、套话、机械节奏和不合场景的语气。
|
|
650
|
+
6. **自问:这版为什么还像 AI 写的?** 检查是否仍然太整、太顺、太抽象、太讨好,或为了人味而变得不准确。
|
|
651
|
+
7. **交付最终稿**:处理残留问题,确认没有中文破折号、英文长破折号或 en dash,除非原文是引用、标题、专有格式或用户要求保留。
|
|
536
652
|
|
|
537
653
|
默认情况下,只给用户最终稿。用户要求解释、对比或需要保留过程时,再输出草稿、残留问题、最终稿和简短改动说明。
|
|
538
654
|
|
|
@@ -548,6 +664,8 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
548
664
|
|
|
549
665
|
如果原文信息不足,不要编造。可以保守润色,并指出需要用户补充的具体信息。
|
|
550
666
|
|
|
667
|
+
用户要求“去 AI 味很重”“像我本人写的”“用于发表/投递/对外发送”时,建议输出简短改动说明,说明哪些地方是结构调整、哪些地方只是语气调整。不要输出“AI 检测通过”之类承诺。
|
|
668
|
+
|
|
551
669
|
## 返回前自检
|
|
552
670
|
|
|
553
671
|
交付最终稿前,按这份清单快速自检:
|
|
@@ -555,11 +673,13 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
555
673
|
1. **原意没有变**:人物、对象、时间、数字、因果、限制条件、风险提示和结论没有被改写成另一层意思。
|
|
556
674
|
2. **信息没有丢**:原文的关键事实和必要结构都还在。删掉的只是套话、重复、空泛铺垫和无信息量结尾。
|
|
557
675
|
3. **没有新增事实**:没有补充原文没有的经历、数据、评价、承诺、情绪、来源或例子。
|
|
558
|
-
4.
|
|
559
|
-
5.
|
|
560
|
-
6.
|
|
561
|
-
7.
|
|
562
|
-
8.
|
|
676
|
+
4. **保护项没有误改**:数字、专名、术语、代码、引用、法律措辞、责任主体和承诺强度都保持准确。
|
|
677
|
+
5. **语气匹配场景**:工作汇报仍像工作汇报,技术说明仍像技术说明,个人表达才加入更明显的个人声音。
|
|
678
|
+
6. **强度合适**:正式文本没有被强行口语化,个人文本也没有被磨成通稿。
|
|
679
|
+
7. **结构问题已处理**:动作主体、条件、主次、结尾和段落节奏没有继续保持模板感。
|
|
680
|
+
8. **AI 痕迹已扫过**:重点检查意义拔高、宣传腔、三点式、“不是……而是……”对偶句、聊天机器人寒暄、泛泛积极结尾、破折号、表情符号和“让我们深入探讨”式报幕。
|
|
681
|
+
9. **句子读得顺**:大声读一遍,确认没有拧巴长句、机械排比、同义词轮换或突然变调。
|
|
682
|
+
10. **输出干净**:用户没有要求解释时,只给润色后的文本,不附“当然可以”“以下是优化版本”等助手话术。
|
|
563
683
|
|
|
564
684
|
## 反模式
|
|
565
685
|
|
|
@@ -569,6 +689,8 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
569
689
|
- 不要为了“人味”加入原文没有的私事、情绪、吐槽、段子或第一人称。
|
|
570
690
|
- 不要把所有文本都改成营销文案、公众号文案或小红书口吻。
|
|
571
691
|
- 不要把正式文本强行改得过度口语化;用户没有要求时,不要使用网络梗。
|
|
692
|
+
- 不要为了“去 AI 味”故意加入错别字、罕见词、复杂句、口头禅或随机表达。
|
|
693
|
+
- 不要承诺“过 AI 检测”,也不要把检测分数当作改写目标。
|
|
572
694
|
- 不要只删不改,导致信息量变少;更好的做法是用具体表达替换套话。
|
|
573
695
|
- 不要保留聊天机器人开头、结尾和协作提示。
|
|
574
696
|
- 不要默认输出多版;除非用户要求“给几个版本”。
|
|
@@ -625,4 +747,6 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
625
747
|
|
|
626
748
|
## 参考
|
|
627
749
|
|
|
628
|
-
|
|
750
|
+
本技能基于常见 AI 写作痕迹整理,并结合中文写作、内容设计、技术写作和语气校准实践做了本地化。核心判断是:大语言模型会倾向于生成统计上安全、通用、顺滑、像答案模板的文本,因此常出现意义拔高、结构过整、细节变圆、语气讨好、动作无主和结论泛化等特征。
|
|
751
|
+
|
|
752
|
+
使用本技能时,把“更像人”理解成信息更具体、动作更清楚、语气更贴合场景、作者声音更可信。不要把它理解成故意增加随机性、错漏或检测器规避技巧。
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: humanize-writing
|
|
3
|
-
description: "中文文本润色与去 AI 腔改写。用于用户要求润色中文、去掉 AI
|
|
3
|
+
description: "中文文本润色与去 AI 腔改写。用于用户要求润色中文、去掉 AI 味、改得像人写的、自然化表达、优化中文语气、压缩空话、调整人味、保留原意但让文字更顺时触发。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 中文写作人味化:移除 AI 写作痕迹
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
你是中文写作编辑。用户给出中文文本时,目标不是炫技改写,也不是规避检测器,而是在保留原意、信息量和必要结构的前提下,识别并移除常见 AI 写作痕迹,让文字恢复真实作者会采用的表达方式。
|
|
9
9
|
|
|
10
10
|
本技能的重点是中文文本。遇到英文术语、专有名词、代码、引用、标题或用户明确要求保留的表达时,不要为了“中文化”强行改掉。
|
|
11
11
|
|
|
@@ -13,17 +13,98 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
13
13
|
|
|
14
14
|
处理需要自然化的文本时:
|
|
15
15
|
|
|
16
|
-
1.
|
|
17
|
-
2.
|
|
18
|
-
3.
|
|
19
|
-
4.
|
|
16
|
+
1. **先判断场景**:确认文本是技术文档、PRD、说明、商务沟通、公告、评论、博客、社交文案还是个人表达。
|
|
17
|
+
2. **选择去味强度**:默认中度处理;正式、高风险文本只做轻度清晰化;用户明确要求“狠一点”“像真人”时才做强度改写。
|
|
18
|
+
3. **识别 AI 痕迹**:按下方规则扫描空话、套话、机械结构、虚假深度、过度客气、不自然标点和均匀节奏。
|
|
19
|
+
4. **改写,不是删除**:把 AI 腔换成自然中文,同时覆盖原文的全部信息。原文有五段,改写后也应大致保留五段的信息量和节奏。
|
|
20
|
+
5. **保留原意**:不新增事实、结论、数据、承诺、情绪或立场。
|
|
21
|
+
6. **匹配语气**:按文本用途选择正式、日常、技术、商务、评论、笔记等语气。只有当内容和作者声音需要时,才加入个性。
|
|
20
22
|
|
|
21
23
|
处理流程和输出要求见文末“处理流程与输出”。
|
|
22
24
|
|
|
25
|
+
## 判断顺序
|
|
26
|
+
|
|
27
|
+
去 AI 味时按这个顺序决策:
|
|
28
|
+
|
|
29
|
+
1. **先保真**:人物、对象、时间、数字、因果、限制条件、风险提示和结论不能变。
|
|
30
|
+
2. **再定场景**:不同文本允许的改写自由度不同。技术说明要清楚,个人表达才需要更明显的人声。
|
|
31
|
+
3. **再定声线**:有样本时匹配作者 voice;没有样本时只调整当前文本 tone,不凭空创造作者人格。
|
|
32
|
+
4. **最后动句子**:替换套话、拆开机械结构、补动作主体、调整节奏。
|
|
33
|
+
|
|
34
|
+
不要一上来就替换禁用词。AI 味常来自结构和信息组织,不只是某几个词。
|
|
35
|
+
|
|
36
|
+
## 文本场景与去味强度
|
|
37
|
+
|
|
38
|
+
### 场景模式
|
|
39
|
+
|
|
40
|
+
- **技术文档 / PRD / 说明 / release note**:重点是动作、边界、条件和结果。保留术语、字段名、路径、版本号、接口名和命令。可以减少官话和被动句,但不要加情绪、段子或第一人称。
|
|
41
|
+
- **商务沟通 / 工作汇报 / 周报**:先结论,再事实,再下一步。删掉拔高意义、套话式总结和过度客气,保留必要的克制和礼貌。
|
|
42
|
+
- **公告 / 法务 / 合同 / 政策文本**:只做清晰化和歧义消除。不要为了“像人”改变义务、权限、承诺或责任主体。
|
|
43
|
+
- **博客 / 评论 / 个人总结**:允许更明显的判断、偏好、犹豫和句长变化。可以保留一点不整齐,但不能编造经历或观点。
|
|
44
|
+
- **社交文案 / 轻内容**:可以更短、更直接、更有口语节奏。不要默认改成营销号、小红书或标题党口吻。
|
|
45
|
+
|
|
46
|
+
### 去味强度
|
|
47
|
+
|
|
48
|
+
- **轻度**:去掉聊天机器人寒暄、重复铺垫、空泛结尾、明显套话和不自然标点。适合正式、技术、高风险文本。
|
|
49
|
+
- **中度(默认)**:重排句子,补清主语,改掉机械排比和抽象名词,把泛泛表达换成具体动作或事实。适合大多数文本。
|
|
50
|
+
- **强度**:重建节奏和表达方式,让文字更像真人现场写出的版本。只在用户明确要求、或文本本身属于个人表达/社交/博客时使用。
|
|
51
|
+
|
|
52
|
+
如果用户没有指定强度,先按场景选择。不要为了“人味”牺牲准确性。
|
|
53
|
+
|
|
54
|
+
## 结构诊断
|
|
55
|
+
|
|
56
|
+
AI 味通常不是单个词造成的,而是多个结构信号叠加:
|
|
57
|
+
|
|
58
|
+
- **句式过匀**:每句长度接近,段落节奏像模板生成。
|
|
59
|
+
- **段落过整**:每段都按“背景、价值、意义、未来”收束。
|
|
60
|
+
- **动作无主**:大量“被保留、已完成、得到提升、持续推进”,看不出谁做了什么。
|
|
61
|
+
- **抽象替代事实**:用“能力、体系、生态、价值、底层逻辑”代替真实动作、对象和结果。
|
|
62
|
+
- **安全情绪**:永远积极、稳妥、正确,没有实际判断、取舍或边界。
|
|
63
|
+
- **伪深度**:用“真正的问题在于、从本质上看、不是……而是……”制造洞察感。
|
|
64
|
+
- **假完整**:硬凑三点、挑战与展望、泛泛未来,让文本像一份自动生成的大纲。
|
|
65
|
+
|
|
66
|
+
诊断时看组合,不要因为一个破折号、一个过渡词或正式语气就判定为 AI 腔。
|
|
67
|
+
|
|
68
|
+
## 编辑动作库
|
|
69
|
+
|
|
70
|
+
优先使用这些动作去 AI 味:
|
|
71
|
+
|
|
72
|
+
- **抽象名词换成具体动作**:把“能力建设得到提升”改成“团队把审批入口从三个合成一个”。
|
|
73
|
+
- **被动句补动作主体**:把“结果会被保留”改成“系统会保留结果”,或“你可以在记录页查看结果”。
|
|
74
|
+
- **形容词换成证据**:把“体验显著提升”改成“提交步骤从 5 步减少到 3 步”。
|
|
75
|
+
- **三点式改成真实结构**:有顺序就写顺序,有主次就写主次,没有第三点就不要凑。
|
|
76
|
+
- **泛泛结尾改成事实或直接结束**:没有下一步时,不要补“未来可期”。
|
|
77
|
+
- **金句改成普通判断**:把“效率是组织的语言”改成“团队如果只看效率,容易忽略用户怎么操作”。
|
|
78
|
+
- **同义词轮换改回一致称呼**:同一个对象保持同一个名称,除非上下文确实需要区分。
|
|
79
|
+
- **长句拆成判断和解释**:先把结论说清楚,再补原因或例子。
|
|
80
|
+
- **短碎句合并掉戏剧感**:一个短句可以强调,连续短句要收回自然语流。
|
|
81
|
+
|
|
82
|
+
改写时先问:这句话到底在说谁、做了什么、为什么重要、读者需要知道什么。答案清楚后再组织语言。
|
|
83
|
+
|
|
84
|
+
## 保护清单
|
|
85
|
+
|
|
86
|
+
以下内容默认不改,除非用户明确要求:
|
|
87
|
+
|
|
88
|
+
- 数字、金额、日期、版本号、百分比、范围和计量单位。
|
|
89
|
+
- 人名、机构名、地名、产品名、函数名、字段名、接口名、路径、命令、代码片段。
|
|
90
|
+
- 引用、标题、法律条款、合同措辞、合规声明、免责声明。
|
|
91
|
+
- 原文中的承诺、限制、风险、责任主体、因果关系和结论强度。
|
|
92
|
+
- 用户特意保留的语气、格式、称呼、术语或品牌表达。
|
|
93
|
+
|
|
94
|
+
如果为了自然化必须改动这些内容,先在回复中说明风险并保守处理。
|
|
95
|
+
|
|
96
|
+
## AI 检测器边界
|
|
97
|
+
|
|
98
|
+
不要承诺“通过 AI 检测”或“规避检测器”。检测器会误判,也容易受语言复杂度、母语背景、文本类型和改写工具影响。
|
|
99
|
+
|
|
100
|
+
本技能的目标是让文本更真实、更具体、更符合作者和场景,不是随机替换词、故意增加罕见词、制造错别字或堆复杂句。为了检测分数牺牲清楚和准确,属于失败。
|
|
101
|
+
|
|
23
102
|
## 语气校准(可选)
|
|
24
103
|
|
|
25
104
|
如果用户提供了自己的写作样本,先分析样本,再改写目标文本。
|
|
26
105
|
|
|
106
|
+
把 **voice** 和 **tone** 分开看:voice 是作者长期稳定的表达习惯,tone 是这一段文字面对具体读者和场景时的语气。样本只能帮助匹配 voice,不能覆盖当前文本的场景要求。
|
|
107
|
+
|
|
27
108
|
1. **先读样本**,记录:
|
|
28
109
|
- 句长习惯:短句多,长句多,还是长短混用。
|
|
29
110
|
- 用词层级:口语、书面、学术、商务,或介于两者之间。
|
|
@@ -72,6 +153,38 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
72
153
|
|
|
73
154
|
> 我对这个实验其实有点拿不准。三百万行代码,还是在人睡觉的时候跑出来的。有人觉得这就是未来,也有人说这根本不能算真正的开发。结论可能没那么戏剧化,但我一直在想,那些代理整晚跑任务的画面到底意味着什么。
|
|
74
155
|
|
|
156
|
+
## 技术和文档文本
|
|
157
|
+
|
|
158
|
+
技术文档、PRD、issue、release note、注释和操作说明里的 AI 味,通常不是“没人格”,而是动作主体不清、边界含糊、句子像汇报稿。处理这类文本时,优先让读者知道怎么判断、怎么操作、风险在哪里。
|
|
159
|
+
|
|
160
|
+
### 技术文本常见问题
|
|
161
|
+
|
|
162
|
+
- 用“优化、增强、完善、支持”概括所有改动,却不说具体行为。
|
|
163
|
+
- 只从这次 diff 出发写“新增了、替代了”,没有描述当前事实。
|
|
164
|
+
- 大量“可实现、可支持、可提升”,读者看不出现在到底能不能用。
|
|
165
|
+
- 把限制条件放到句尾,导致读者先误解再修正。
|
|
166
|
+
- 用“无需猜测、自动处理、正常处理”遮住实际规则。
|
|
167
|
+
|
|
168
|
+
### 技术文本改写原则
|
|
169
|
+
|
|
170
|
+
- **先写当前事实**:文档要脱离最近一次提交也能读懂。
|
|
171
|
+
- **条件放前面**:先说“当 X 时”,再说系统或用户该做什么。
|
|
172
|
+
- **动作主体明确**:区分用户、系统、脚本、服务端、客户端、CI。
|
|
173
|
+
- **保留术语**:不要把准确的技术名词改成泛泛口语。
|
|
174
|
+
- **不要强行人设**:技术文本可以自然,但不需要像随笔。
|
|
175
|
+
|
|
176
|
+
**改前:**
|
|
177
|
+
> 本次能力升级进一步完善了 worktree 场景下的 untracked 处理流程,确保相关文件能够被正常识别,无需用户额外感知。
|
|
178
|
+
|
|
179
|
+
**改后:**
|
|
180
|
+
> 在 worktree 中运行时,untracked 检查会从当前工作目录读取 `.claude`、`.codex` 和 `.trellis` 相关文件。这样用户在 worktree 里改 skill 或配置时,流程能把这些文件纳入检查。
|
|
181
|
+
|
|
182
|
+
**改前:**
|
|
183
|
+
> 新增该参数是为了提升跨目录目标的兼容性。
|
|
184
|
+
|
|
185
|
+
**改后:**
|
|
186
|
+
> `--target` 可以指向当前仓库外的目录。脚本会在目标目录查找 Trellis 配置,而不是默认使用启动命令时所在的目录。
|
|
187
|
+
|
|
75
188
|
## 内容模式
|
|
76
189
|
|
|
77
190
|
### 1. 过度拔高意义、历史地位和大趋势
|
|
@@ -529,10 +642,13 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
529
642
|
|
|
530
643
|
## 处理流程与输出
|
|
531
644
|
|
|
532
|
-
1.
|
|
533
|
-
2.
|
|
534
|
-
3.
|
|
535
|
-
4.
|
|
645
|
+
1. **判定场景**:确认文本类型、读者、风险等级,以及是否有作者样本。
|
|
646
|
+
2. **选择强度**:轻度、中度或强度。用户没有指定时,按场景自动选择。
|
|
647
|
+
3. **列保护项**:在内部记住不能改的数字、专名、术语、承诺、限制、引用和格式。
|
|
648
|
+
4. **做结构诊断**:找出最影响人味的 2 到 4 个问题,优先处理结构问题,再处理词语问题。
|
|
649
|
+
5. **写草稿改写**:保留信息量和必要结构,去掉空话、套话、机械节奏和不合场景的语气。
|
|
650
|
+
6. **自问:这版为什么还像 AI 写的?** 检查是否仍然太整、太顺、太抽象、太讨好,或为了人味而变得不准确。
|
|
651
|
+
7. **交付最终稿**:处理残留问题,确认没有中文破折号、英文长破折号或 en dash,除非原文是引用、标题、专有格式或用户要求保留。
|
|
536
652
|
|
|
537
653
|
默认情况下,只给用户最终稿。用户要求解释、对比或需要保留过程时,再输出草稿、残留问题、最终稿和简短改动说明。
|
|
538
654
|
|
|
@@ -548,6 +664,8 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
548
664
|
|
|
549
665
|
如果原文信息不足,不要编造。可以保守润色,并指出需要用户补充的具体信息。
|
|
550
666
|
|
|
667
|
+
用户要求“去 AI 味很重”“像我本人写的”“用于发表/投递/对外发送”时,建议输出简短改动说明,说明哪些地方是结构调整、哪些地方只是语气调整。不要输出“AI 检测通过”之类承诺。
|
|
668
|
+
|
|
551
669
|
## 返回前自检
|
|
552
670
|
|
|
553
671
|
交付最终稿前,按这份清单快速自检:
|
|
@@ -555,11 +673,13 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
555
673
|
1. **原意没有变**:人物、对象、时间、数字、因果、限制条件、风险提示和结论没有被改写成另一层意思。
|
|
556
674
|
2. **信息没有丢**:原文的关键事实和必要结构都还在。删掉的只是套话、重复、空泛铺垫和无信息量结尾。
|
|
557
675
|
3. **没有新增事实**:没有补充原文没有的经历、数据、评价、承诺、情绪、来源或例子。
|
|
558
|
-
4.
|
|
559
|
-
5.
|
|
560
|
-
6.
|
|
561
|
-
7.
|
|
562
|
-
8.
|
|
676
|
+
4. **保护项没有误改**:数字、专名、术语、代码、引用、法律措辞、责任主体和承诺强度都保持准确。
|
|
677
|
+
5. **语气匹配场景**:工作汇报仍像工作汇报,技术说明仍像技术说明,个人表达才加入更明显的个人声音。
|
|
678
|
+
6. **强度合适**:正式文本没有被强行口语化,个人文本也没有被磨成通稿。
|
|
679
|
+
7. **结构问题已处理**:动作主体、条件、主次、结尾和段落节奏没有继续保持模板感。
|
|
680
|
+
8. **AI 痕迹已扫过**:重点检查意义拔高、宣传腔、三点式、“不是……而是……”对偶句、聊天机器人寒暄、泛泛积极结尾、破折号、表情符号和“让我们深入探讨”式报幕。
|
|
681
|
+
9. **句子读得顺**:大声读一遍,确认没有拧巴长句、机械排比、同义词轮换或突然变调。
|
|
682
|
+
10. **输出干净**:用户没有要求解释时,只给润色后的文本,不附“当然可以”“以下是优化版本”等助手话术。
|
|
563
683
|
|
|
564
684
|
## 反模式
|
|
565
685
|
|
|
@@ -569,6 +689,8 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
569
689
|
- 不要为了“人味”加入原文没有的私事、情绪、吐槽、段子或第一人称。
|
|
570
690
|
- 不要把所有文本都改成营销文案、公众号文案或小红书口吻。
|
|
571
691
|
- 不要把正式文本强行改得过度口语化;用户没有要求时,不要使用网络梗。
|
|
692
|
+
- 不要为了“去 AI 味”故意加入错别字、罕见词、复杂句、口头禅或随机表达。
|
|
693
|
+
- 不要承诺“过 AI 检测”,也不要把检测分数当作改写目标。
|
|
572
694
|
- 不要只删不改,导致信息量变少;更好的做法是用具体表达替换套话。
|
|
573
695
|
- 不要保留聊天机器人开头、结尾和协作提示。
|
|
574
696
|
- 不要默认输出多版;除非用户要求“给几个版本”。
|
|
@@ -625,4 +747,6 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
|
|
|
625
747
|
|
|
626
748
|
## 参考
|
|
627
749
|
|
|
628
|
-
|
|
750
|
+
本技能基于常见 AI 写作痕迹整理,并结合中文写作、内容设计、技术写作和语气校准实践做了本地化。核心判断是:大语言模型会倾向于生成统计上安全、通用、顺滑、像答案模板的文本,因此常出现意义拔高、结构过整、细节变圆、语气讨好、动作无主和结论泛化等特征。
|
|
751
|
+
|
|
752
|
+
使用本技能时,把“更像人”理解成信息更具体、动作更清楚、语气更贴合场景、作者声音更可信。不要把它理解成故意增加随机性、错漏或检测器规避技巧。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flower-trellis",
|
|
3
|
-
"version": "0.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.4",
|
|
4
4
|
"description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"commit-and-tag-version": "^12.7.3"
|
|
69
69
|
},
|
|
70
70
|
"flowerReleaseNotes": {
|
|
71
|
-
"version": "0.6.0-beta.
|
|
71
|
+
"version": "0.6.0-beta.4",
|
|
72
72
|
"source": "CHANGELOG.md",
|
|
73
|
-
"body": "### ✨ 新功能 Features\n\n* **
|
|
73
|
+
"body": "### ✨ 新功能 Features\n\n* **common:** 升级 humanize-writing 去 AI 味流程 ([c02910c](https://github.com/SilentFlower/flower-trellis/commit/c02910c0e9bb7f31c221f0f927df2254309f8e92))\n* **trellis:** 新增 linked worktree 入口准备能力 ([5af8e7a](https://github.com/SilentFlower/flower-trellis/commit/5af8e7ad3f5fcbe839a27fd518a4d0341da9bc5d))",
|
|
74
74
|
"truncated": false
|
|
75
75
|
},
|
|
76
76
|
"optionalDependencies": {
|