chore: 升级 Trellis 并精简规则(09-05-trellis-upgrade-rule-cleanup)
这个提交包含在:
@@ -92,9 +92,16 @@ def _detect_platform(input_data: dict) -> str | None:
|
||||
return "codex"
|
||||
if isinstance(input_data.get("cursor_version"), str):
|
||||
return "cursor"
|
||||
# CLAUDE_PROJECT_DIR is a compatibility alias that several hosts set
|
||||
# alongside their own variable — CodeBuddy, ZCode and Trae all do. It must
|
||||
# therefore be checked LAST, or every one of them is detected as claude and
|
||||
# the context key becomes `claude_<their-session-id>`. That key does not
|
||||
# match the session file `task.py start` wrote under the host's real name,
|
||||
# so the sub-agent starts with no task context while the pointer exists on
|
||||
# disk. Same fix as inject-workflow-state.py and session-start.py; this
|
||||
# third copy was missed when those two were corrected.
|
||||
env_map = {
|
||||
"ZCODE_PROJECT_DIR": "zcode",
|
||||
"CLAUDE_PROJECT_DIR": "claude",
|
||||
"CURSOR_PROJECT_DIR": "cursor",
|
||||
"CODEBUDDY_PROJECT_DIR": "codebuddy",
|
||||
"FACTORY_PROJECT_DIR": "droid",
|
||||
@@ -102,6 +109,9 @@ def _detect_platform(input_data: dict) -> str | None:
|
||||
"QODER_PROJECT_DIR": "qoder",
|
||||
"KIRO_PROJECT_DIR": "kiro",
|
||||
"COPILOT_PROJECT_DIR": "copilot",
|
||||
"TRAE_PROJECT_DIR": "trae",
|
||||
# Last: the shared alias, only meaningful once no vendor key matched.
|
||||
"CLAUDE_PROJECT_DIR": "claude",
|
||||
}
|
||||
for env_name, platform in env_map.items():
|
||||
if os.environ.get(env_name):
|
||||
@@ -238,9 +248,33 @@ class _Budget:
|
||||
self.used += size
|
||||
|
||||
|
||||
def _real_path_contained(base_real: str, target_real: str) -> bool:
|
||||
"""Whether an already-realpath'd target sits under an already-realpath'd base.
|
||||
|
||||
ValueError on Windows when the two sit on different drives; that is
|
||||
outside the base by definition, so it fails closed.
|
||||
"""
|
||||
try:
|
||||
return os.path.commonpath([base_real, target_real]) == base_real
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _read_file_bytes(base_path: str, file_path: str) -> bytes | None:
|
||||
"""Read raw file bytes, return None if file doesn't exist."""
|
||||
full_path = os.path.join(base_path, file_path)
|
||||
try:
|
||||
root_real = os.path.realpath(base_path)
|
||||
# `.trellis` may itself be a symlink into a store outside the repo
|
||||
# (#567); its real location is a second legitimate containment base.
|
||||
workflow_real = os.path.realpath(os.path.join(base_path, ".trellis"))
|
||||
full_real = os.path.realpath(full_path)
|
||||
if not _real_path_contained(root_real, full_real) and not (
|
||||
_real_path_contained(workflow_real, full_real)
|
||||
):
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
if os.path.exists(full_path) and os.path.isfile(full_path):
|
||||
try:
|
||||
with open(full_path, "rb") as f:
|
||||
@@ -365,12 +399,12 @@ def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[dict]:
|
||||
Schema:
|
||||
{"file": "path/to/file.md", "reason": "..."}
|
||||
{"file": "path/to/dir/", "type": "directory", "reason": "..."}
|
||||
{"_example": "..."} # seed row — skipped (no `file` field)
|
||||
{"_example": "..."} # legacy placeholder — skipped (no `file` field)
|
||||
|
||||
Rows without a ``file`` field (e.g. the self-describing seed line written
|
||||
by ``task.py create`` before the agent has curated entries) are skipped
|
||||
silently. If the resulting entry list is empty, a stderr warning is
|
||||
emitted so the operator can debug missing context.
|
||||
Rows without a ``file`` field (e.g. the placeholder line older Trellis
|
||||
versions wrote at ``task.py create`` time) are skipped silently. If the
|
||||
resulting entry list is empty, a stderr warning is emitted so the operator
|
||||
can debug missing context.
|
||||
|
||||
Returns:
|
||||
[{"file": path, "type": "file" | "directory", "reason": reason}, ...]
|
||||
@@ -459,6 +493,17 @@ def get_agent_context(
|
||||
"""
|
||||
agent_jsonl = f"{task_dir}/{agent_type}.jsonl"
|
||||
blocks = _materialize_jsonl_entries(repo_root, agent_jsonl, limits, budget)
|
||||
if not blocks:
|
||||
# Zero curated context reaches the model silently otherwise — the
|
||||
# stderr WARN above never enters any session (#573). Put the fact in
|
||||
# the prompt itself so the sub-agent compensates instead of assuming
|
||||
# the spec context was complete.
|
||||
return (
|
||||
f"[Trellis] {agent_jsonl} has no curated entries, so no spec/research "
|
||||
"context was injected. Before working, read the guidelines relevant "
|
||||
"to the code you will touch under .trellis/spec/, and treat the task "
|
||||
"artifacts below as the only prepared context."
|
||||
)
|
||||
return "\n\n".join(blocks)
|
||||
|
||||
|
||||
@@ -877,8 +922,15 @@ def _handle_codex_subagent_start(input_data: dict) -> None:
|
||||
if not subagent_type or not parent_session_id:
|
||||
return
|
||||
|
||||
cwd = _string_value(input_data.get("cwd")) or os.getcwd()
|
||||
repo_root = find_repo_root(cwd)
|
||||
# Payload cwd first, then our own — some hosts (CodeBuddy IDE 4.10.4)
|
||||
# report "/" for every hook event. See inject-workflow-state.py.
|
||||
repo_root = None
|
||||
for candidate in (_string_value(input_data.get("cwd")), os.getcwd()):
|
||||
if not candidate:
|
||||
continue
|
||||
repo_root = find_repo_root(candidate)
|
||||
if repo_root:
|
||||
break
|
||||
if not repo_root:
|
||||
return
|
||||
|
||||
@@ -979,6 +1031,8 @@ def _extract_subagent_type(tool_input: dict) -> str:
|
||||
"subagentType",
|
||||
"subagent_type_name",
|
||||
"subagentTypeName",
|
||||
"subagent_name",
|
||||
"subagentName",
|
||||
"agent_type",
|
||||
"agentType",
|
||||
"name",
|
||||
@@ -994,7 +1048,8 @@ def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]:
|
||||
|
||||
Returns (subagent_type, original_prompt, tool_input).
|
||||
Handles:
|
||||
- Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type
|
||||
- Claude Code / Qoder / Droid: tool_name=Task|Agent, tool_input.subagent_type
|
||||
- CodeBuddy: tool_name=task (IDE) or Task (CLI), tool_input.subagent_name
|
||||
- Cursor: tool_name=Task|Subagent, tool_input.subagent_type
|
||||
- Copilot CLI: toolName=task (camelCase key, lowercase value)
|
||||
- ZCode: toolName=Agent, toolInput/tool_input.subagent_type
|
||||
@@ -1073,8 +1128,24 @@ def main():
|
||||
if subagent_type in AGENTS_REQUIRE_TASK:
|
||||
if not task_dir:
|
||||
sys.exit(0)
|
||||
# Check if task directory exists
|
||||
task_dir_full = os.path.join(repo_root, task_dir)
|
||||
# Contain the pointer before reading anything through it. `task.py` now
|
||||
# refuses to store a ref that leaves the repo, but a session file
|
||||
# written before that fix can still hold one, and `trellis update`
|
||||
# does not rewrite session files — so a poisoned pointer outlives the
|
||||
# upgrade that closed the writer. This is the last hop before the
|
||||
# task's prd.md/design.md reach the model prompt, so it checks again.
|
||||
try:
|
||||
root_real = os.path.realpath(repo_root)
|
||||
# `.trellis` may itself be a symlink into a store outside the
|
||||
# repo (#567); its real location is a second legitimate base.
|
||||
workflow_real = os.path.realpath(os.path.join(repo_root, ".trellis"))
|
||||
task_dir_full = os.path.realpath(os.path.join(repo_root, task_dir))
|
||||
if not _real_path_contained(root_real, task_dir_full) and not (
|
||||
_real_path_contained(workflow_real, task_dir_full)
|
||||
):
|
||||
sys.exit(0)
|
||||
except OSError:
|
||||
sys.exit(0)
|
||||
if not os.path.exists(task_dir_full):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户