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.
@@ -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\"."
@@ -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 _find_repo_root(start: Path | None = None) -> Path | None:
86
- """从当前路径向上查找 Trellis 项目根目录。"""
87
- current = (start or Path.cwd()).resolve()
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-04T02:44:38.844Z",
2
+ "syncedAt": "2026-08-04T10:14:37.206Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "32477325abe35122e9b12d4cc257fab9c7576c48",
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
- 你是中文写作编辑。用户给出中文文本时,目标不是炫技改写,而是在保留原意、信息量和必要结构的前提下,识别并移除常见 AI 写作痕迹,让文字更像真实的人写出来。
8
+ 你是中文写作编辑。用户给出中文文本时,目标不是炫技改写,也不是规避检测器,而是在保留原意、信息量和必要结构的前提下,识别并移除常见 AI 写作痕迹,让文字恢复真实作者会采用的表达方式。
9
9
 
10
10
  本技能的重点是中文文本。遇到英文术语、专有名词、代码、引用、标题或用户明确要求保留的表达时,不要为了“中文化”强行改掉。
11
11
 
@@ -13,17 +13,98 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
13
13
 
14
14
  处理需要自然化的文本时:
15
15
 
16
- 1. **识别 AI 痕迹**:按下方规则扫描空话、套话、机械结构、虚假深度、过度客气和不自然标点。
17
- 2. **改写,不是删除**:把 AI 腔换成自然中文,同时覆盖原文的全部信息。原文有五段,改写后也应大致保留五段的信息量和节奏。
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. 自问:**“下面这版为什么还像 AI 写的?”** 简短列出残留问题。
535
- 4. 继续改成**最终稿**,处理残留问题,并确保最终稿没有中文破折号、英文长破折号或 en dash。
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. **AI 痕迹已扫过**:重点检查意义拔高、宣传腔、三点式、“不是……而是……”对偶句、聊天机器人寒暄、泛泛积极结尾、破折号、表情符号和“让我们深入探讨”式报幕。
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
- 本技能基于 Wikipedia “Signs of AI writing” 页面整理,并结合中文写作场景做了本地化。核心判断是:大语言模型会倾向于生成统计上最安全、最通用、最像答案模板的文本,因此常出现意义拔高、结构过整、细节变圆、语气讨好和结论泛化等特征。
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
- 你是中文写作编辑。用户给出中文文本时,目标不是炫技改写,而是在保留原意、信息量和必要结构的前提下,识别并移除常见 AI 写作痕迹,让文字更像真实的人写出来。
8
+ 你是中文写作编辑。用户给出中文文本时,目标不是炫技改写,也不是规避检测器,而是在保留原意、信息量和必要结构的前提下,识别并移除常见 AI 写作痕迹,让文字恢复真实作者会采用的表达方式。
9
9
 
10
10
  本技能的重点是中文文本。遇到英文术语、专有名词、代码、引用、标题或用户明确要求保留的表达时,不要为了“中文化”强行改掉。
11
11
 
@@ -13,17 +13,98 @@ description: "中文文本润色与去 AI 腔改写。用于用户要求润色
13
13
 
14
14
  处理需要自然化的文本时:
15
15
 
16
- 1. **识别 AI 痕迹**:按下方规则扫描空话、套话、机械结构、虚假深度、过度客气和不自然标点。
17
- 2. **改写,不是删除**:把 AI 腔换成自然中文,同时覆盖原文的全部信息。原文有五段,改写后也应大致保留五段的信息量和节奏。
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. 自问:**“下面这版为什么还像 AI 写的?”** 简短列出残留问题。
535
- 4. 继续改成**最终稿**,处理残留问题,并确保最终稿没有中文破折号、英文长破折号或 en dash。
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. **AI 痕迹已扫过**:重点检查意义拔高、宣传腔、三点式、“不是……而是……”对偶句、聊天机器人寒暄、泛泛积极结尾、破折号、表情符号和“让我们深入探讨”式报幕。
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
- 本技能基于 Wikipedia “Signs of AI writing” 页面整理,并结合中文写作场景做了本地化。核心判断是:大语言模型会倾向于生成统计上最安全、最通用、最像答案模板的文本,因此常出现意义拔高、结构过整、细节变圆、语气讨好和结论泛化等特征。
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",
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.3",
71
+ "version": "0.6.0-beta.4",
72
72
  "source": "CHANGELOG.md",
73
- "body": "### ✨ 新功能 Features\n\n* **check-all:** 同步 Check-All fact-status 文档修复规则 ([8b48a5c](https://github.com/SilentFlower/flower-trellis/commit/8b48a5c5b074ad614351726a85525853352da990))\n* **update:** 增加启动更新提示节流控制 ([d341841](https://github.com/SilentFlower/flower-trellis/commit/d34184194e260359a427da72205621579577ab65))\n\n\n### 🐛 修复 Bug Fixes\n\n* **trellis:** 同步 untracked_flow 入口限制 ([4732835](https://github.com/SilentFlower/flower-trellis/commit/47328352035b9d4c5e9972a95dd457e4d7eb9217))\n* **trellis:** 衔接 untracked 检查、规范更新和推送链路 ([9f1e3b1](https://github.com/SilentFlower/flower-trellis/commit/9f1e3b16af627b9c95a373047a146d07c6ecda72))",
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": {