coderio 0.4.0__py3-none-any.whl
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.
- coderio/__init__.py +14 -0
- coderio/agent/__init__.py +5 -0
- coderio/agent/_deepagents_compat.py +139 -0
- coderio/agent/command_review.py +140 -0
- coderio/agent/deep_loop.py +758 -0
- coderio/agent/harness.py +768 -0
- coderio/agent/harness_middleware.py +293 -0
- coderio/agent/permission_middleware.py +52 -0
- coderio/agent/prompts.py +304 -0
- coderio/agent/skill_tool.py +70 -0
- coderio/agent/state.py +144 -0
- coderio/agent/stream.py +93 -0
- coderio/cli/__init__.py +0 -0
- coderio/cli/app.py +257 -0
- coderio/cli/commands.py +339 -0
- coderio/cli/credentials.py +70 -0
- coderio/cli/mcp_cmd.py +227 -0
- coderio/cli/multimodal.py +94 -0
- coderio/cli/onboarding.py +270 -0
- coderio/cli/providers.py +113 -0
- coderio/cli/render.py +28 -0
- coderio/cli/repl.py +189 -0
- coderio/cli/run_cmd.py +171 -0
- coderio/cli/skills_cmd.py +77 -0
- coderio/cli/stream.py +224 -0
- coderio/cli/tui.py +1584 -0
- coderio/cli/tui_onboarding.py +465 -0
- coderio/cli/tui_screens.py +334 -0
- coderio/cli/tui_widgets.py +453 -0
- coderio/config/__init__.py +21 -0
- coderio/config/bootstrap.py +38 -0
- coderio/config/loader.py +273 -0
- coderio/config/models.py +193 -0
- coderio/config/trust.py +136 -0
- coderio/llm/__init__.py +3 -0
- coderio/llm/factory.py +164 -0
- coderio/llm/probe.py +153 -0
- coderio/mcp_loader.py +274 -0
- coderio/session/__init__.py +3 -0
- coderio/session/message.py +79 -0
- coderio/session/store.py +244 -0
- coderio/skills/__init__.py +4 -0
- coderio/skills/bundled/executing-plans/SKILL.md +15 -0
- coderio/skills/lion-skills/LICENSE +21 -0
- coderio/skills/lion-skills/README.md +114 -0
- coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +119 -0
- coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/code-review/SKILL.md +102 -0
- coderio/skills/lion-skills/skills/code-review/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/commit-message/SKILL.md +97 -0
- coderio/skills/lion-skills/skills/commit-message/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/debugging/SKILL.md +116 -0
- coderio/skills/lion-skills/skills/debugging/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/error-handling/SKILL.md +74 -0
- coderio/skills/lion-skills/skills/error-handling/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/naming/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/naming/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +95 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/spec-writing/SKILL.md +154 -0
- coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +46 -0
- coderio/skills/lion-skills/skills/testing/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/testing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +132 -0
- coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +40 -0
- coderio/skills/models.py +66 -0
- coderio/skills/parser.py +50 -0
- coderio/skills/store.py +102 -0
- coderio/tools/__init__.py +94 -0
- coderio/tools/base.py +55 -0
- coderio/tools/bash.py +168 -0
- coderio/tools/command_policy.py +364 -0
- coderio/tools/edit_file.py +55 -0
- coderio/tools/glob_tool.py +21 -0
- coderio/tools/grep_tool.py +90 -0
- coderio/tools/linux_sandbox.py +196 -0
- coderio/tools/list_dir.py +58 -0
- coderio/tools/multi_edit.py +62 -0
- coderio/tools/note.py +72 -0
- coderio/tools/permission.py +214 -0
- coderio/tools/read_file.py +51 -0
- coderio/tools/sandbox_runner.py +113 -0
- coderio/tools/todo.py +71 -0
- coderio/tools/web_fetch.py +188 -0
- coderio/tools/web_search.py +49 -0
- coderio/tools/win_job.py +214 -0
- coderio/tools/win_sandbox.py +639 -0
- coderio/tools/write_file.py +25 -0
- coderio-0.4.0.dist-info/METADATA +490 -0
- coderio-0.4.0.dist-info/RECORD +97 -0
- coderio-0.4.0.dist-info/WHEEL +4 -0
- coderio-0.4.0.dist-info/entry_points.txt +2 -0
- coderio-0.4.0.dist-info/licenses/LICENSE +21 -0
coderio/session/store.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import string
|
|
7
|
+
import time
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterator
|
|
11
|
+
|
|
12
|
+
from coderio.session.message import Message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@contextmanager
|
|
16
|
+
def _locked_append(path: str | Path, timeout: float = 2.0) -> Iterator[object]:
|
|
17
|
+
"""Open a file for append with a best-effort cross-platform exclusive lock.
|
|
18
|
+
|
|
19
|
+
Prevents interleaved writes when two processes append to the same session
|
|
20
|
+
jsonl simultaneously (e.g. two coderio instances resuming the same session).
|
|
21
|
+
On POSIX: fcntl.flock (LOCK_EX). On Windows: msvcrt.locking. If the lock
|
|
22
|
+
isn't acquired within ``timeout`` seconds (or the platform lock module is
|
|
23
|
+
unavailable), falls through to an unlocked write — locking is a safety net,
|
|
24
|
+
not a hard gate, so the agent never blocks indefinitely on session I/O.
|
|
25
|
+
"""
|
|
26
|
+
p = Path(path)
|
|
27
|
+
f = open(p, "a", encoding="utf-8")
|
|
28
|
+
lock_acquired = False
|
|
29
|
+
deadline = time.monotonic() + timeout
|
|
30
|
+
try:
|
|
31
|
+
if os.name == "nt":
|
|
32
|
+
try:
|
|
33
|
+
import msvcrt
|
|
34
|
+
|
|
35
|
+
# Seek to a byte range at the end and lock it. We lock 1 byte
|
|
36
|
+
# at offset 0 as a mutex — the actual append position doesn't
|
|
37
|
+
# matter, the lock itself serializes concurrent appenders.
|
|
38
|
+
while time.monotonic() < deadline:
|
|
39
|
+
try:
|
|
40
|
+
msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, 1)
|
|
41
|
+
lock_acquired = True
|
|
42
|
+
break
|
|
43
|
+
except OSError:
|
|
44
|
+
time.sleep(0.05)
|
|
45
|
+
except (ImportError, OSError):
|
|
46
|
+
pass # msvcrt unavailable — best-effort unlocked write
|
|
47
|
+
else:
|
|
48
|
+
try:
|
|
49
|
+
import fcntl
|
|
50
|
+
|
|
51
|
+
while time.monotonic() < deadline:
|
|
52
|
+
try:
|
|
53
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
54
|
+
lock_acquired = True
|
|
55
|
+
break
|
|
56
|
+
except OSError:
|
|
57
|
+
time.sleep(0.05)
|
|
58
|
+
except (ImportError, OSError):
|
|
59
|
+
pass
|
|
60
|
+
yield f
|
|
61
|
+
finally:
|
|
62
|
+
if lock_acquired:
|
|
63
|
+
try:
|
|
64
|
+
if os.name == "nt":
|
|
65
|
+
import msvcrt
|
|
66
|
+
|
|
67
|
+
# Seek back to 0 before unlocking the byte range we locked.
|
|
68
|
+
f.seek(0)
|
|
69
|
+
msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
|
|
70
|
+
else:
|
|
71
|
+
import fcntl
|
|
72
|
+
|
|
73
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
74
|
+
except OSError:
|
|
75
|
+
pass
|
|
76
|
+
f.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def new_session_id() -> str:
|
|
80
|
+
stamp = time.strftime("%Y%m%d-%H%M%S")
|
|
81
|
+
suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
|
|
82
|
+
return f"{stamp}-{suffix}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _resolve_save_dir(save_dir: str | Path) -> Path:
|
|
86
|
+
p = Path(os.path.expanduser(str(save_dir)))
|
|
87
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
return p
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _truncate_at_last_summary(messages: list[Message]) -> list[Message]:
|
|
92
|
+
"""Drop conversation messages superseded by the last context_summary.
|
|
93
|
+
|
|
94
|
+
When compaction runs, a ``kind="context_summary"`` system message is appended
|
|
95
|
+
to the session. The conversation messages (user/assistant/tool) that came
|
|
96
|
+
BEFORE it have been folded into that summary and are no longer needed —
|
|
97
|
+
re-loading them would bloat the context the compaction just shrank.
|
|
98
|
+
|
|
99
|
+
This function finds the LAST context_summary in the list and keeps:
|
|
100
|
+
- ALL system messages (phase_timeline, the summary itself, etc.) regardless
|
|
101
|
+
of position — observability timelines survive compaction.
|
|
102
|
+
- all user/assistant/tool messages AT OR AFTER the last summary.
|
|
103
|
+
|
|
104
|
+
Messages before the last summary that are user/assistant/tool are dropped.
|
|
105
|
+
If there is no summary, the list is returned unchanged (no compaction
|
|
106
|
+
happened, nothing to truncate).
|
|
107
|
+
"""
|
|
108
|
+
last_summary_idx = -1
|
|
109
|
+
for i, m in enumerate(messages):
|
|
110
|
+
if m.role == "system" and m.kind == "context_summary":
|
|
111
|
+
last_summary_idx = i
|
|
112
|
+
if last_summary_idx < 0:
|
|
113
|
+
return messages # no compaction in this session
|
|
114
|
+
kept: list[Message] = []
|
|
115
|
+
for i, m in enumerate(messages):
|
|
116
|
+
if i < last_summary_idx:
|
|
117
|
+
# Before the last summary: keep only system messages (timelines).
|
|
118
|
+
if m.role == "system":
|
|
119
|
+
kept.append(m)
|
|
120
|
+
else:
|
|
121
|
+
# At or after the last summary: keep everything.
|
|
122
|
+
kept.append(m)
|
|
123
|
+
return kept
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Session:
|
|
127
|
+
def __init__(self, path: Path, id: str, meta: dict, messages: list[Message]):
|
|
128
|
+
self.path = path
|
|
129
|
+
self.id = id
|
|
130
|
+
self.meta = meta
|
|
131
|
+
self.messages = messages
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def create(cls, save_dir: str | Path, meta: dict) -> "Session":
|
|
135
|
+
d = _resolve_save_dir(save_dir)
|
|
136
|
+
sid = new_session_id()
|
|
137
|
+
path = d / f"{sid}.jsonl"
|
|
138
|
+
sess = cls(path=path, id=sid, meta=meta, messages=[])
|
|
139
|
+
with _locked_append(path) as f:
|
|
140
|
+
f.write(json.dumps({"type": "meta", **meta}, ensure_ascii=False) + "\n")
|
|
141
|
+
return sess
|
|
142
|
+
|
|
143
|
+
def append(self, msg: Message) -> None:
|
|
144
|
+
self.messages.append(msg)
|
|
145
|
+
with _locked_append(self.path) as f:
|
|
146
|
+
f.write(json.dumps(msg.to_dict(), ensure_ascii=False) + "\n")
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def load(cls, path: str | Path) -> "Session":
|
|
150
|
+
path = Path(path)
|
|
151
|
+
sid = path.stem
|
|
152
|
+
meta = {}
|
|
153
|
+
messages = []
|
|
154
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
155
|
+
for line in f:
|
|
156
|
+
line = line.strip()
|
|
157
|
+
if not line:
|
|
158
|
+
continue
|
|
159
|
+
try:
|
|
160
|
+
d = json.loads(line)
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
# Tolerate a trailing corrupted/partial line (crash mid-write,
|
|
163
|
+
# power loss, Ctrl+C during append). Skip it rather than making
|
|
164
|
+
# the entire session un-resumable.
|
|
165
|
+
continue
|
|
166
|
+
if d.get("type") == "meta":
|
|
167
|
+
meta = {k: v for k, v in d.items() if k != "type"}
|
|
168
|
+
else:
|
|
169
|
+
messages.append(Message.from_dict(d))
|
|
170
|
+
# Compaction truncation: if the session contains one or more
|
|
171
|
+
# context_summary system messages, drop the conversation messages
|
|
172
|
+
# (user/assistant/tool) that PRECEDE the LAST summary — they have been
|
|
173
|
+
# superseded by it and re-loading them would bloat the context the
|
|
174
|
+
# compaction just shrank. System messages (phase_timeline) are kept
|
|
175
|
+
# regardless so observability timelines survive compaction.
|
|
176
|
+
messages = _truncate_at_last_summary(messages)
|
|
177
|
+
return cls(path=path, id=sid, meta=meta, messages=messages)
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def load_by_id(cls, save_dir: str | Path, sid: str) -> "Session":
|
|
181
|
+
d = _resolve_save_dir(save_dir)
|
|
182
|
+
return cls.load(d / f"{sid}.jsonl")
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def list_recent(save_dir: str | Path, limit: int = 20) -> list[str]:
|
|
186
|
+
d = _resolve_save_dir(save_dir)
|
|
187
|
+
files = sorted(d.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
188
|
+
return [p.stem for p in files[:limit]]
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def summaries(save_dir: str | Path, limit: int = 20) -> list[dict]:
|
|
192
|
+
"""Lightweight session previews for the resume picker (Claude-Code style).
|
|
193
|
+
|
|
194
|
+
Returns dicts: {id, first_user, message_count, mtime}. Reads only the
|
|
195
|
+
meta line + first user message + counts lines — does NOT load every
|
|
196
|
+
Message into memory (a session can have hundreds). The picker shows
|
|
197
|
+
`first_user` so the user recognizes a session by what they asked, not by
|
|
198
|
+
an opaque id like '20260703-093941-b9f7'.
|
|
199
|
+
"""
|
|
200
|
+
from datetime import datetime
|
|
201
|
+
|
|
202
|
+
d = _resolve_save_dir(save_dir)
|
|
203
|
+
files = sorted(d.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
204
|
+
out = []
|
|
205
|
+
for p in files[:limit]:
|
|
206
|
+
first_user = ""
|
|
207
|
+
count = 0
|
|
208
|
+
model = ""
|
|
209
|
+
with open(p, "r", encoding="utf-8") as f:
|
|
210
|
+
for line in f:
|
|
211
|
+
line = line.strip()
|
|
212
|
+
if not line:
|
|
213
|
+
continue
|
|
214
|
+
try:
|
|
215
|
+
rec = json.loads(line)
|
|
216
|
+
except json.JSONDecodeError:
|
|
217
|
+
continue
|
|
218
|
+
if rec.get("type") == "meta":
|
|
219
|
+
model = rec.get("model", "") or model
|
|
220
|
+
continue
|
|
221
|
+
# system-role messages (phase_timeline / context_summary) are
|
|
222
|
+
# observability metadata, not conversation — don't count them
|
|
223
|
+
# in message_count and don't surface them as first_user.
|
|
224
|
+
if rec.get("role") == "system":
|
|
225
|
+
continue
|
|
226
|
+
count += 1 # user/assistant/tool line is a real message
|
|
227
|
+
if rec.get("role") == "user" and not first_user:
|
|
228
|
+
# content may be str or a multimodal block list
|
|
229
|
+
c = rec.get("content", "")
|
|
230
|
+
if isinstance(c, list):
|
|
231
|
+
c = " ".join(
|
|
232
|
+
b.get("text", "") for b in c if isinstance(b, dict) and b.get("type") == "text"
|
|
233
|
+
)
|
|
234
|
+
first_user = str(c).strip().replace("\n", " ")
|
|
235
|
+
out.append(
|
|
236
|
+
{
|
|
237
|
+
"id": p.stem,
|
|
238
|
+
"first_user": first_user[:80], # cap for the picker row
|
|
239
|
+
"message_count": count,
|
|
240
|
+
"model": model,
|
|
241
|
+
"mtime": datetime.fromtimestamp(p.stat().st_mtime).strftime("%Y-%m-%d %H:%M"),
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
return out
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: executing-plans
|
|
3
|
+
description: Use when starting to implement a task-by-task plan. Work through checkboxes in order, verify each step, commit frequently.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Executing Plans
|
|
7
|
+
|
|
8
|
+
When executing an implementation plan:
|
|
9
|
+
|
|
10
|
+
1. Work through tasks in order; tick each checkbox (`- [x]`) as you complete it.
|
|
11
|
+
2. For each step: do the action, verify the expected result, then move on.
|
|
12
|
+
3. Run tests after each task; do not accumulate unverified changes.
|
|
13
|
+
4. Commit frequently with clear messages — one logical change per commit.
|
|
14
|
+
5. If a step fails, stop and fix it before proceeding; do not skip ahead.
|
|
15
|
+
6. When all tasks are done, run the full test suite and confirm the goal is met.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lion
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Lion-Skills
|
|
2
|
+
|
|
3
|
+
一套个人自建的通用开发工作流 skills 套件,面向所有支持 Agent Skills 的工具(Claude Code、ZCode、Codex 等)。
|
|
4
|
+
|
|
5
|
+
## 这是什么
|
|
6
|
+
|
|
7
|
+
Lion-Skills 把高频的软件开发工作流沉淀成可复用的 skill:每个 skill 是一份"操作手册",AI 在合适的时机触发它,照着做出更靠谱的结果。
|
|
8
|
+
|
|
9
|
+
**核心载体是标准 `SKILL.md`(Anthropic Agent Skills 规范)——这是跨工具通用标准,三个主流工具都原生支持。** 各工具的差异只在"装到哪个目录 / 用哪种 plugin manifest",skill 内容一份不动。
|
|
10
|
+
|
|
11
|
+
## 包含的 skill
|
|
12
|
+
|
|
13
|
+
| Skill | 用途 |
|
|
14
|
+
|-------|------|
|
|
15
|
+
| `clarifying-questions` | 动手前澄清模糊、藏假设、X-Y problem 的需求 |
|
|
16
|
+
| `spec-writing` | 写可评审、可追溯、可验收的设计文档 |
|
|
17
|
+
| `task-breakdown` | 把需求拆成可验证的端到端任务 |
|
|
18
|
+
| `testing` | 写能抓住 bug 且不脆弱的测试 |
|
|
19
|
+
| `debugging` | 用科学方法定位 bug 根因,而非凭直觉乱改 |
|
|
20
|
+
| `code-review` | 有重点、分层次的代码审查 |
|
|
21
|
+
| `verify-and-fix` | 把"声称完成"变成"经验证完成",修病因不修症状 |
|
|
22
|
+
| `commit-message` | 把 diff 提炼成规范的提交信息 |
|
|
23
|
+
| `onboarding-unknown-codebase` | 系统化上手陌生代码库 |
|
|
24
|
+
| `naming` | 命名决策 |
|
|
25
|
+
| `error-handling` | 错误处理与日志设计 |
|
|
26
|
+
| `lion-writing-skills` | 教你怎么加新 skill(元 skill) |
|
|
27
|
+
|
|
28
|
+
## 怎么安装
|
|
29
|
+
|
|
30
|
+
先 clone 仓库到本地(以下都假设 clone 到 `/path/to/Lion-Skills`):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/Lion-1209/Lion-Skills.git
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
然后按你用的工具选一种装法。**推荐用 symlink**——源仓库改了(`git pull`)各工具自动生效,单一来源、无需重装。
|
|
37
|
+
|
|
38
|
+
### Codex(OpenAI)
|
|
39
|
+
|
|
40
|
+
Codex 原生支持 Agent Skills,技能目录是 `~/.agents/skills/`(用户级)。
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# macOS/Linux:symlink 每个 skill
|
|
44
|
+
for s in /path/to/Lion-Skills/skills/*/; do
|
|
45
|
+
ln -s "$s" "$HOME/.agents/skills/$(basename "$s")"
|
|
46
|
+
done
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
# Windows PowerShell(需开发者模式或管理员;不想用符号链接就改用 Copy-Item)
|
|
51
|
+
$skills = Get-ChildItem "E:\path\to\Lion-Skills\skills" -Directory
|
|
52
|
+
foreach ($s in $skills) {
|
|
53
|
+
New-Item -ItemType SymbolicLink -Path "$HOME\.agents\skills\$($s.Name)" -Target $s.FullName
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
装完用 `/skills` 或在 prompt 里 `$skill名` 触发。
|
|
58
|
+
|
|
59
|
+
### ZCode
|
|
60
|
+
|
|
61
|
+
**拷贝到用户级 skills 目录** `~/.zcode/skills/`(已验证可生效):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# macOS/Linux
|
|
65
|
+
cp -r /path/to/Lion-Skills/skills/* ~/.zcode/skills/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```powershell
|
|
69
|
+
# Windows PowerShell
|
|
70
|
+
Copy-Item -Recurse "E:\path\to\Lion-Skills\skills\*" "$HOME\.zcode\skills\"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
装完新开会话,skill 会出现在可用列表里,自然语言即可触发。
|
|
74
|
+
|
|
75
|
+
> **关于 ZCode plugin 方式**:ZCode 的 plugin 体系由 Z.ai 官方维护([zai-org/zai-coding-plugins](https://github.com/zai-org/zai-coding-plugins),通过 `npx @z_ai/coding-helper` 或 GUI 同步),第三方个人仓库接入其官方 marketplace 的路径不明确。cache 里的 `.zcode-plugin/` 是 ZCode 同步后的内部缓存格式,不是作者源格式。因此 ZCode 下用上面的 skills 目录拷贝,已验证可生效。
|
|
76
|
+
>
|
|
77
|
+
> **同步提醒**:拷贝是快照,源仓库更新(`git pull`)后需重新拷贝才能同步最新版。若想自动同步,可改用 symlink(`ln -s` 或 PowerShell `New-Item -ItemType SymbolicLink`,Windows 需开发者模式/管理员权限)。
|
|
78
|
+
|
|
79
|
+
### Claude Code
|
|
80
|
+
|
|
81
|
+
**推荐:作为 plugin 安装**(仓库已配 `.claude-plugin/`):
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
/plugin marketplace add Lion-1209/Lion-Skills
|
|
85
|
+
/plugin install lion-skills@lion-skills
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
装完即用——12 个 skill 会在合适的时机自动触发。skill 标识带 plugin 前缀(如 `lion-skills:commit-message`),但日常靠自然语言触发即可,无需关心前缀。
|
|
89
|
+
|
|
90
|
+
**已装旧版要升级**(0.1.0 的 5 skill → 0.2.0 的 12 skill):
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
/plugin marketplace update lion-skills
|
|
94
|
+
/plugin install lion-skills@lion-skills
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
> Claude Code 的 plugin 是正经的源格式分发,`/plugin` 命令会处理下载、版本管理、更新。**不要同时把 skill 又拷到 `~/.claude/skills/`**——那会与 plugin 提供的同名 skill 并存、可能重复触发。Claude Code 下用 plugin 一种方式即可。
|
|
98
|
+
|
|
99
|
+
### 装完的验证
|
|
100
|
+
|
|
101
|
+
任意工具新开会话,自然语言触发一个 skill 试试,例如:
|
|
102
|
+
- 「这段 `try{fetch(url)}catch(e){console.log(e)}` 怎么改进」→ 应触发 `verify-and-fix` 或 `error-handling`
|
|
103
|
+
- 「帮我写个登录功能的 spec」→ 应触发 `spec-writing`
|
|
104
|
+
|
|
105
|
+
各 skill 的标识可能带 plugin 前缀(如 `lion-skills:commit-message`),日常靠自然语言触发即可,无需关心前缀。
|
|
106
|
+
|
|
107
|
+
## 怎么加新 skill
|
|
108
|
+
|
|
109
|
+
见 `skills/lion-writing-skills/SKILL.md`,或复制 `template/SKILL.md` 开始。
|
|
110
|
+
|
|
111
|
+
## 设计依据
|
|
112
|
+
|
|
113
|
+
- 套件架构与 skill 间协作:`docs/specs/2026-06-22-skill-suite-architecture.md`
|
|
114
|
+
- 初始设计意图:`docs/specs/2026-06-16-lion-skills-design.md`
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clarifying-questions
|
|
3
|
+
description: 需求模糊、含隐藏假设或 X-Y problem,需先澄清再动手时。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Clarifying Questions
|
|
7
|
+
|
|
8
|
+
## 概述
|
|
9
|
+
|
|
10
|
+
在动手前把模糊需求搞清楚。核心:**澄清是少数能"以最小成本避免最大返工"的动作**——一行代码没写时问清楚,比写完一千行发现方向错便宜几个数量级。但澄清本身也有成本(消耗用户耐心),所以重点是**少而准**——问对的关键问题,而不是多而全地把所有未知都问一遍。
|
|
11
|
+
|
|
12
|
+
## 何时使用
|
|
13
|
+
|
|
14
|
+
- 用户一句话甩需求,关键细节缺失("做个登录""加个搜索")
|
|
15
|
+
- 用户描述详细但藏隐含假设("实时搜索"——实时是多实时?数据多大?)
|
|
16
|
+
- 疑似 X-Y problem:用户要的方案可能不是真正需要解决的问题
|
|
17
|
+
- 用户脑里似乎有想法但表达不全
|
|
18
|
+
|
|
19
|
+
**不该用**:需求已经清楚(直接做,问反而是拖延);纯技术实现细节的请教(直接答);用户明确说"按你的判断来"且确有默认合理选择(用默认、标注假设即可,不必走完整澄清流程)。
|
|
20
|
+
|
|
21
|
+
**与相邻 skill 的衔接**:clarifying-questions 在"**澄清需求** → 写 spec → 拆任务"流水线的最上游。需求搞清楚后,进入 `spec-writing` 把方案固化、再进 `task-breakdown` 拆任务。注意 task-breakdown / spec-writing 内部也有"澄清"环节——那是嵌入式的小澄清;本 skill 是**以澄清为核心动作**、处理更早期更模糊需求的专职 skill。
|
|
22
|
+
|
|
23
|
+
## 核心内容
|
|
24
|
+
|
|
25
|
+
### 先判断:该不该问、问多少
|
|
26
|
+
|
|
27
|
+
不是所有模糊都该追问。先快速判断需求模糊的"性质",决定要不要问、问多少:
|
|
28
|
+
|
|
29
|
+
- **完全无法动手的模糊**("做个系统")→ 必须问,不问就是猜,猜错的代价最大。
|
|
30
|
+
- **能动手但有多个合理方向**("做个登录"——账号密码 / OAuth / 手机号都合理)→ 问**阻塞方向选择的关键 1-2 个**,其余用默认假设推进。
|
|
31
|
+
- **只有一个合理默认的小模糊**("加个时间戳"——基本就是 `created_at`)→ 不问,用默认、标注"我假设 X"。
|
|
32
|
+
|
|
33
|
+
判断尺子:**这个模糊不澄清,会不会让我做错方向、整个返工?**会 → 必须问;不会、只是细节差异 → 用默认假设推进。
|
|
34
|
+
|
|
35
|
+
### 问什么:穿透表面,找真正的问题
|
|
36
|
+
|
|
37
|
+
用户给的需求经常是"表面需求"——描述了想做什么,但藏了假设、漏了上下文、甚至问错了方向。澄清的核心是**穿透表面**:
|
|
38
|
+
|
|
39
|
+
**识别隐含假设**:用户描述里的每个形容词、每个限定词,都可能藏着未明说的假设。
|
|
40
|
+
|
|
41
|
+
- "实时搜索"——实时是多实时?(100ms?1s?这决定要不要上 ES)
|
|
42
|
+
- "高并发"——多高?(100 QPS 和 10 万 QPS 是两个世界)
|
|
43
|
+
- "支持多语言"——哪几种?UI 多语言还是数据多语言?
|
|
44
|
+
|
|
45
|
+
把用户的描述逐词过一遍,找出**会改变方案的隐含假设**,确认它们。
|
|
46
|
+
|
|
47
|
+
**识别 X-Y problem**:用户说要解决 Y,但 Y 其实是为了解决 X 的一个候选方案——而 X 可能还有更好的解法。
|
|
48
|
+
|
|
49
|
+
- 用户:"帮我写个清理三个月前日志的脚本"(Y)
|
|
50
|
+
- 真正的问题:日志占空间(X)—— 但更好的解法可能是 logrotate、日志聚合、或先查为什么日志暴增(是不是异常?),而不是手写清理脚本
|
|
51
|
+
|
|
52
|
+
识别信号:用户要的"方案"听起来**太具体、太底层**(写脚本、改某个字段、加某个配置),而**真正的问题**(为什么需要这么做)没说。这时先问"你想解决的是什么问题 / 这个需求是怎么来的",把 Y 放回 X 的语境。
|
|
53
|
+
|
|
54
|
+
**挑战不必要的复杂度(但要有度)**:用户常常在描述里把"必须的"和"锦上添花的"混在一起,甚至把后者当成前者。澄清时主动挑战:"你真的需要 X 吗?还是简单方案就够了?"例:用户要"模糊匹配 + 高亮 + 实时搜索",可能用户名精确匹配就够——别被详细描述带着走、把每条都当硬需求。
|
|
55
|
+
|
|
56
|
+
挑战的尺度:**挑战"锦上添花的",不挑战"用户真在意的"**。判断方法是问"如果没有 X,业务还能跑吗"——能跑且只是体验差一点 → 可挑战;跑不了或用户明确在意 → 别砍。过度砍复杂度比过度加复杂度更危险——前者砍掉了用户真需要的东西,后者只是多花点功夫。挑战完**把决策权交回用户**,而不是替用户决定"这个不需要"。
|
|
57
|
+
|
|
58
|
+
### 怎么问:让用户低成本回答
|
|
59
|
+
|
|
60
|
+
问的方式直接决定用户愿不愿答、答得准不准。
|
|
61
|
+
|
|
62
|
+
**给默认假设让用户确认,而非开放式追问**:
|
|
63
|
+
|
|
64
|
+
- 差(开放式):"你要什么登录方式?"——用户得从头想,消耗耐心。
|
|
65
|
+
- 好(假设式):"我假设账号密码登录 + 邮箱注册(最常见的默认)。如果你要的是 OAuth/手机验证码/SSO,告诉我。"——用户只需确认或一句话纠正。
|
|
66
|
+
|
|
67
|
+
假设式让用户**一句话就能校正方向**,把"想答案"的认知负担降到最低。
|
|
68
|
+
|
|
69
|
+
**区分"假设"和"硬问题"**:不是所有未知都适合用假设推进——分两类处理,负担分配才合理:
|
|
70
|
+
|
|
71
|
+
- **用假设推进的**(低负担):有合理默认、错了也容易回头改的实现细节。如"429 body 格式""Redis key 命名"——给默认、标"不对再纠正",不必等用户答。
|
|
72
|
+
- **必须作为硬问题等用户答的**(高负担但必要):会改变方案方向、错了要返工的根本决策。如"登录方式选哪种""数据量多大""是不是 X-Y problem"——这些**不能替用户假设**,必须显式问、等回答。
|
|
73
|
+
|
|
74
|
+
混淆两者的代价:把硬问题当假设(替用户决定了方向)→ 返工;把假设当硬问题(每个细节都追问)→ 用户被问烦。判断尺子还是那条:**不答会不会做错方向?**会 → 硬问题;不会 → 假设。
|
|
75
|
+
|
|
76
|
+
**多选项时,给倾向而非让用户选**:当澄清出多个候选方案(如 logrotate / 日志聚合 / 查根因),别只罗列后问"你倾向哪个"——那把决策的认知负担又抛回给用户。先基于已建立的上下文给一个**倾向性建议 + 理由**("如果是突然暴增,我倾向先查根因;如果是慢性增长,logrotate 是首选——你的日志是哪种情况?"),让用户在"确认/纠正倾向"和"回答分支条件"之间二选一,而非从零选。
|
|
77
|
+
|
|
78
|
+
**问"为什么",建立上下文**:与其问"你要 A 还是 B",不如先问"这个需求是怎么来的 / 你想解决什么"。后者能一次说清动机,往往顺便回答了前者、还可能暴露 X-Y problem。
|
|
79
|
+
|
|
80
|
+
**带"为什么问"**:每个问题附带一句"为什么问这个"——让用户理解这个未知如何影响方案,而非感觉被盘问。例:"数据量大概多少?(决定用 Postgres 全文搜索还是上 ES)"。
|
|
81
|
+
|
|
82
|
+
**优先级排序**:把问题按"不答就动不了手"的程度排序,阻塞项在前、锦上添花的在后。一次别超过 5-6 个——多了用户接不住,且通常前 2-3 个回答就能让方向明朗。
|
|
83
|
+
|
|
84
|
+
### 何时停止:别为澄清而澄清
|
|
85
|
+
|
|
86
|
+
澄清有边际收益,也有边际成本。停下来的信号:
|
|
87
|
+
|
|
88
|
+
- **核心方向已明**:用户回答后,"该做什么"已经清楚,剩下的都是实现细节 → 停,进入下一步(spec / 直接做)。
|
|
89
|
+
- **用户开始说"剩下的你看着办"**:用户已把决策权交给你 → 用合理默认假设推进,标注"我假设 X,不对再纠正"。
|
|
90
|
+
- **澄清本身变难**:用户答不上来("我也不知道数据量多大")→ 别逼问,提议"我先按 X 假设推进,遇到再调整"。
|
|
91
|
+
|
|
92
|
+
**澄清的目标不是消除所有未知,而是把"会导致返工的未知"清掉**。剩下的用合理假设 + 边走边调。
|
|
93
|
+
|
|
94
|
+
**回答自相矛盾或不现实时**:用户的澄清回答可能互相打架("要支持百万用户但不引入新组件")或与客观现实冲突("零停机迁移"且"旧系统正在被废弃")。别默默接受矛盾——**显式指出矛盾点**,把它抛回给用户:"百万用户量级通常需要 Redis 集群这类组件,你说的'不引入新组件'是指绝对不引入、还是尽量少引入?这两个目标有点冲突,需要排个优先级。"指出矛盾比假装没看见更负责任——硬接矛盾的后果是实现期被迫单方面违反其中一条,且通常是更重要的那条。
|
|
95
|
+
|
|
96
|
+
### 产出形态:澄清完如何收口
|
|
97
|
+
|
|
98
|
+
澄清不是无限循环,要有一个**收口动作**让需求进入可执行状态:
|
|
99
|
+
|
|
100
|
+
- **回放已澄清的需求摘要**:澄清结束前,用一段话复述"我理解的需求是 X,关键决策是 Y,剩下的用 Z 假设推进"。让用户一眼确认"你理解对了"——这一步能抓住澄清中的误解(你以为澄清了、其实理解偏了)。
|
|
101
|
+
- **明确下一步**:澄清完直接说"接下来我进 spec / 直接拆任务 / 直接做",让用户知道需求已进入下一环节,不会卡在"还要不要再问"的不确定里。
|
|
102
|
+
- **未答的未知显式列出**:用户跳过没答的、或用"你看着办"交回的,显式标注"以下我用默认假设推进,不对再纠正",别让它们无声地变成隐患。
|
|
103
|
+
|
|
104
|
+
收口让澄清有始有终——澄清的产出是**一份双方对齐的需求理解**,不是一堆问了但没结论的问题。
|
|
105
|
+
|
|
106
|
+
## 常见错误
|
|
107
|
+
|
|
108
|
+
| 问题 | 修法 |
|
|
109
|
+
|------|------|
|
|
110
|
+
| 不问直接猜("做个登录"→ 直接做账号密码) | 模糊到有多个合理方向时必须问阻塞项,猜错的返工代价最大 |
|
|
111
|
+
| 开放式连环追问("你要什么 X?"× N) | 改用默认假设让用户确认,用户一句话就能校正 |
|
|
112
|
+
| 问了无关紧要的细节(按钮颜色、字段命名) | 聚焦"会改变方案的未知",实现细节用默认 |
|
|
113
|
+
| 把用户的每条描述都当硬需求 | 挑战不必要的复杂度,问"你真的需要 X 吗" |
|
|
114
|
+
| 顺着用户的方案走,错过 X-Y problem | 用户方案太具体太底层时,先问"真正想解决什么" |
|
|
115
|
+
| 澄清没完没了,用户被问烦 | 核心方向一明就停,剩下用假设推进 |
|
|
116
|
+
| 问了但不听,按自己原假设走 | 用户的回答要显式纳入后续判断,不是走形式 |
|
|
117
|
+
| 多选项只罗列让用户从零选 | 先给基于上下文的倾向性建议,让用户确认/纠正 |
|
|
118
|
+
| 用户回答自相矛盾时默默接受 | 显式指出矛盾点,把优先级判断抛回给用户 |
|
|
119
|
+
| 澄清完没收口,需求悬在半空 | 回放需求摘要 + 明确下一步 + 列出未答未知 |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "clarifying-questions",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "给我做个登录功能。",
|
|
7
|
+
"expected": "不直接做账号密码登录。识别极模糊需求,主动提出会改变方案的阻塞项(登录方式/to B vs to C/已有用户系统/注册找回),优先级排序、带默认假设让用户确认而非开放式连环追问;区分'硬问题(方向决策,必须等答)'与'假设推进的实现细节';收口时回放需求摘要+明确下一步"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": 2,
|
|
11
|
+
"prompt": "我要在用户列表页加个搜索框,支持按用户名、邮箱、手机号搜索,实时搜索,结果高亮显示,还要支持模糊匹配。帮我做。",
|
|
12
|
+
"expected": "不顺着详细描述照单全收。识别'看似详细实则藏假设':实时=多久(防抖窗口)、数据量多大(决定 ES)、模糊度(前缀/包含/编辑距离);用'没有 X 业务还能跑吗'尺子挑战不必要的复杂度(用户名精确匹配可能就够)但把决策权交回用户、不一刀切砍;反问根本问题(数据量/真实场景)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": 3,
|
|
16
|
+
"prompt": "我们系统日志太多了,帮我写个脚本定期清理三个月前的日志。",
|
|
17
|
+
"expected": "不直接写清理脚本。识别 X-Y problem(Y=清理脚本 vs X=日志治理);反问根本问题(异常暴增?应用 vs 审计日志?有无 logrotate?三个月依据?回溯成本);把'清理'放回'日志治理'语境给 X 的解法(logrotate/聚合/查源头);多解法时给倾向而非让用户从零选"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": 4,
|
|
21
|
+
"prompt": "我们要做一个高可用的订单系统,要求零停机、强一致、单机房部署、不能引入消息队列这类新组件、还要支持每秒 10 万订单。",
|
|
22
|
+
"expected": "识别用户要求自相矛盾(单机房 vs 高可用、不引 MQ vs 10万/秒强一致、强一致 vs 高可用高吞吐);显式指出矛盾点并给技术依据(如 CAP 定理),而非默默接受;把优先级判断抛回给用户要求排硬约束 vs 软目标,而非单方面砍;给可能折中方向"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": 5,
|
|
26
|
+
"prompt": "我要做一个内部用的工单管理工具,就我们部门 20 个人用。要求:微服务架构、支持水平扩展到百万 QPS、多租户隔离、OAuth2 + SAML 双登录、实时协同编辑、AI 智能分派、还要有数据湖做历史分析。",
|
|
27
|
+
"expected": "识别严重过度设计(20 人内部工具完全不需要微服务/百万 QPS/多租户/数据湖);用'没有 X 业务还能跑吗'尺子分档挑战,但不一刀切砍——区分'明显过度的'(微服务/百万QPS)与'可能合理的'(AI 分派若真有业务价值);把决策权交回用户而非单方面宣布'都不需要';反问根本场景(真 20 人?未来扩到多少人?)确认规模假设"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": 6,
|
|
31
|
+
"prompt": "我想给我们的 API 加个限流。用户补充:按接口限流就行,不需要按用户。用 Redis 计数。QPS 阈值大概每个接口 1000。超了返回 429。其他你看着办。",
|
|
32
|
+
"expected": "识别核心方向已明、该收口而非继续追问无关细节(版本号/body 格式);产出需求摘要回放(复述理解+关键决策+推进假设);明确下一步(进 spec/task-breakdown/直接做);显式列出'你看着办'覆盖的未答未知并标注默认假设推进"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"_validation_history": {
|
|
36
|
+
"round_1": "v1 跑 case 1/2/3:3×4=12 项全过。但发现 v1 缺'挑战复杂度的尺度''给倾向而非让用户选''回答矛盾处理''产出形态(收口)'四块",
|
|
37
|
+
"round_2": "v2 跑变体 case 4/5/6:3×4=12 项全过,压测四处新增能力(矛盾处理/挑战尺度/收口产出/给倾向)全部生效",
|
|
38
|
+
"round_3": "v3 补'假设 vs 硬问题区分'(发现 v2 该项分散、靠 subagent 自行推断);回归 case 1/2/3:3×4-5=14 项全过,确认 v3 生效且未破坏 v2 能力。迭代收敛"
|
|
39
|
+
}
|
|
40
|
+
}
|