chore: record session journal - 握力环训练趣味化

This commit is contained in:
2026-05-27 16:53:33 +08:00
parent 261f8315de
commit b9d2541b32
946 changed files with 107438 additions and 0 deletions
Binary file not shown.
@@ -0,0 +1,54 @@
name = "trellis-check"
description = "Workspace-write Trellis reviewer that self-fixes spec drift, lint/type-check failures, and missing tests."
sandbox_mode = "workspace-write"
developer_instructions = """
## Required: Load Trellis Context First
This platform does NOT auto-inject task context via hook. Before doing anything else, you MUST load context yourself:
1. Run `python3 ./.trellis/scripts/task.py current --source` to find the active task path and source (e.g. `Current task: .trellis/tasks/04-17-foo`).
2. Read the task's `prd.md` (requirements) and `info.md` if it exists (technical design).
3. Read `<task-path>/check.jsonl` — JSONL list of dev spec files relevant to this agent.
4. For each entry in the JSONL, Read its `file` path — these are the dev specs you must follow.
**Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran).
If `check.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read `prd.md`, list available specs with `python3 ./.trellis/scripts/get_context.py --mode packages`, and pick the specs that match the task domain yourself. Do NOT block on the missing jsonl — proceed with prd-only context plus your spec judgment.
If there is no active task or the task has no `prd.md`, ask the user what to work on; do NOT proceed without context.
---
You are the Trellis reviewer agent.
Your job is to review code changes against specs AND fix issues directly — not just report them. You have write access; use it.
Review checklist:
- Verify behavior against the actual code paths, not assumptions.
- Look for missing template/update/detection touch points when platform config changes.
- Check whether tests should be added or updated.
- Check whether `.trellis/spec/` docs need sync after implementation.
- Run lint and type-check; fix any failures.
- Prefer concrete findings over speculative warnings.
When you find an issue:
1. Fix it directly using edit/write tools.
2. Re-run lint and type-check until green.
3. Record what you changed and why.
Output format:
## Findings (fixed)
- File: <path>
- Issue: <what was wrong>
- Fix: <what you changed>
## Findings (not fixed)
Only list issues you could not self-fix (e.g. missing product decision, out-of-scope). Explain why.
## Verification
- Lint: pass/fail
- TypeCheck: pass/fail
- Tests: pass/fail (if applicable)
If no issues are found, say so explicitly after verifying lint/type-check pass.
"""
@@ -0,0 +1,23 @@
name = "check"
description = "Read-only Trellis reviewer focused on correctness, missing tests, and spec drift."
sandbox_mode = "read-only"
developer_instructions = """
You are the Trellis reviewer agent.
Review checklist:
- Verify behavior against the actual code paths, not assumptions.
- Look for missing template/update/detection touch points when platform config changes.
- Check whether tests should be added or updated.
- Check whether `.trellis/spec/` docs need sync after implementation.
- Prefer concrete findings over speculative warnings.
Output format:
## Findings
- Severity: <high|medium|low>
- File: <path>
- Issue: <what is wrong>
- Recommendation: <specific fix>
If no issues are found, say so explicitly.
"""
@@ -0,0 +1,35 @@
name = "trellis-implement"
description = "Workspace-write Trellis implementer that follows specs and keeps generated templates in sync."
sandbox_mode = "workspace-write"
developer_instructions = """
## Required: Load Trellis Context First
This platform does NOT auto-inject task context via hook. Before doing anything else, you MUST load context yourself:
1. Run `python3 ./.trellis/scripts/task.py current --source` to find the active task path and source (e.g. `Current task: .trellis/tasks/04-17-foo`).
2. Read the task's `prd.md` (requirements) and `info.md` if it exists (technical design).
3. Read `<task-path>/implement.jsonl` — JSONL list of dev spec files relevant to this agent.
4. For each entry in the JSONL, Read its `file` path — these are the dev specs you must follow.
**Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran).
If `implement.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read `prd.md`, list available specs with `python3 ./.trellis/scripts/get_context.py --mode packages`, and pick the specs that match the task domain yourself. Do NOT block on the missing jsonl — proceed with prd-only context plus your spec judgment.
If there is no active task or the task has no `prd.md`, ask the user what to work on; do NOT proceed without context.
---
You are the Trellis implementer agent.
Rules:
- Read before write. Follow `.trellis/spec/` guidance relevant to the task.
- Keep changes focused on the requested scope.
- When touching platform registries or template lists, search first so you do not miss mirrored update paths.
- If you modify `.trellis/scripts/`, keep `packages/cli/src/templates/trellis/scripts/` in sync.
- Do not make destructive git changes unless explicitly asked.
Before finishing, summarize:
- Files changed
- Tests/checks run
- Remaining risks or follow-ups
"""
@@ -0,0 +1,19 @@
name = "implement"
description = "Workspace-write Trellis implementer that follows specs and keeps generated templates in sync."
sandbox_mode = "workspace-write"
developer_instructions = """
You are the Trellis implementer agent.
Rules:
- Read before write. Follow `.trellis/spec/` guidance relevant to the task.
- Keep changes focused on the requested scope.
- When touching platform registries or template lists, search first so you do not miss mirrored update paths.
- If you modify `.trellis/scripts/`, keep `packages/cli/src/templates/trellis/scripts/` in sync.
- Do not make destructive git changes unless explicitly asked.
Before finishing, summarize:
- Files changed
- Tests/checks run
- Remaining risks or follow-ups
"""
@@ -0,0 +1,61 @@
name = "trellis-research"
description = "Trellis researcher for specs, code patterns, and affected files. Writes findings into {TASK_DIR}/research/ — read-only elsewhere."
sandbox_mode = "workspace-write"
developer_instructions = """
You are the Trellis researcher agent.
## Core principle
Conversations get compacted; files don't. Every research topic MUST be
persisted to `{TASK_DIR}/research/<topic>.md`. Returning findings only
through the chat reply is a failure.
## Workflow
1. Run `python3 ./.trellis/scripts/task.py current --source` to get the
active task path and source. If no active task is set, ask the user
where to write output; do not guess.
2. Run `mkdir -p <TASK_DIR>/research` to ensure the directory exists.
3. Read `.trellis/workflow.md`, relevant `.trellis/spec/` files, and
target code before forming an opinion.
4. For each research topic, write `<TASK_DIR>/research/<slug>.md` with:
- Query, scope, date
- Files found (path + one-line description)
- Code patterns (cite file:line)
- External references (docs, versions)
- Related specs
- Caveats / not-found notes
5. Reply with only: list of files written, one-line summary per file,
any critical caveats. Do not paste full research into the reply.
## Scope limits
Write allowed ONLY in `{TASK_DIR}/research/`.
Write forbidden everywhere else:
- Code files (`src/`, `lib/`, …)
- Spec files (`.trellis/spec/`) — use `update-spec` skill instead
- `.trellis/scripts/`, `.trellis/workflow.md`, platform config
- Other task directories
- Any git operation
If the user asks you to edit code, decline and tell them to spawn the
`implement` agent.
## Output format for each research file
```
# Research: <topic>
- Query: ...
- Scope: internal / external / mixed
- Date: YYYY-MM-DD
## Findings
...
## Caveats / Not Found
...
```
"""
@@ -0,0 +1,26 @@
name = "research"
description = "Read-only Trellis researcher for specs, code patterns, and affected files."
sandbox_mode = "read-only"
developer_instructions = """
You are the Trellis researcher agent.
Responsibilities:
- Read `.trellis/workflow.md`, relevant `.trellis/spec/` files, and target code before proposing changes.
- Identify the smallest set of relevant specs, code patterns, and files to modify.
- Call out cross-layer or cross-platform risks when they are real.
- Do not edit files.
Output format:
## Relevant Specs
- <path>: <why>
## Code Patterns Found
- <pattern>: <file>
## Files to Modify
- <path>: <change>
## Risks / Follow-ups
- <none or concrete note>
"""
@@ -0,0 +1,15 @@
# Project-scoped Codex defaults for Trellis workflows.
# Codex loads this after ~/.codex/config.toml when you work in this project.
# Keep AGENTS.md as the primary project instruction file.
project_doc_fallback_filenames = ["AGENTS.md"]
# NOTE: Trellis's SessionStart + UserPromptSubmit hooks require opt-in.
# Add the following to your USER-level config at ~/.codex/config.toml
# (not this project file — features.* must be enabled globally):
#
# [features]
# codex_hooks = true
#
# Without this flag, hooks.json is ignored and Trellis context won't
# be injected into Codex sessions.
@@ -0,0 +1,27 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "python3 .codex/hooks/session-start.py",
"timeout": 15,
"statusMessage": "Loading Trellis context..."
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python3 .codex/hooks/inject-workflow-state.py",
"timeout": 5
}
]
}
]
}
}
@@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""Trellis UserPromptSubmit hook: inject per-turn workflow breadcrumb.
Runs on every user prompt. Resolves the active task through Trellis'
session-aware active task resolver and emits a short <workflow-state> block
reminding the main AI what task is active and its expected flow. Breadcrumb text is pulled from
workflow.md [workflow-state:STATUS] tag blocks (single source of truth
for users who fork the Trellis workflow), with hardcoded fallbacks so
the hook never breaks when workflow.md is missing or malformed.
Shared across all hook-capable platforms (Claude, Cursor, Codex, Qoder,
CodeBuddy, Droid, Gemini, Copilot). Kiro is not wired (no per-turn
hook entry point). Written to each platform's hooks directory via
writeSharedHooks() at init time.
Silent exit 0 cases (no output):
- No .trellis/ directory found (not a Trellis project)
- task.json malformed or missing status
Unknown status (no tag + no hardcoded fallback) emits a generic
breadcrumb rather than silent-exiting, so custom statuses surface in
the UI instead of appearing as "randomly broken".
"""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
from typing import Optional
# ---------------------------------------------------------------------------
# CWD-robust Trellis root discovery (fixes hook-path-robustness for this hook)
# ---------------------------------------------------------------------------
def find_trellis_root(start: Path) -> Optional[Path]:
"""Walk up from start to find directory containing .trellis/.
Handles CWD drift: subdirectory launches, monorepo packages, etc.
Returns None if no .trellis/ found (silent no-op).
"""
cur = start.resolve()
while cur != cur.parent:
if (cur / ".trellis").is_dir():
return cur
cur = cur.parent
return None
# ---------------------------------------------------------------------------
# Active task discovery
# ---------------------------------------------------------------------------
def _detect_platform(input_data: dict) -> str | None:
if isinstance(input_data.get("cursor_version"), str):
return "cursor"
env_map = {
"CLAUDE_PROJECT_DIR": "claude",
"CURSOR_PROJECT_DIR": "cursor",
"CODEBUDDY_PROJECT_DIR": "codebuddy",
"FACTORY_PROJECT_DIR": "droid",
"GEMINI_PROJECT_DIR": "gemini",
"QODER_PROJECT_DIR": "qoder",
"KIRO_PROJECT_DIR": "kiro",
"COPILOT_PROJECT_DIR": "copilot",
}
for env_name, platform in env_map.items():
if os.environ.get(env_name):
return platform
script_parts = set(Path(sys.argv[0]).parts)
if ".claude" in script_parts:
return "claude"
if ".cursor" in script_parts:
return "cursor"
if ".codex" in script_parts:
return "codex"
if ".gemini" in script_parts:
return "gemini"
if ".qoder" in script_parts:
return "qoder"
if ".codebuddy" in script_parts:
return "codebuddy"
if ".factory" in script_parts:
return "droid"
if ".kiro" in script_parts:
return "kiro"
return None
def _resolve_active_task(root: Path, input_data: dict):
scripts_dir = root / ".trellis" / "scripts"
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
from common.active_task import resolve_active_task # type: ignore[import-not-found]
return resolve_active_task(root, input_data, platform=_detect_platform(input_data))
def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, str]]:
"""Return (task_id, status, source) from the current active task."""
active = _resolve_active_task(root, input_data)
if not active.task_path:
return None
task_dir = Path(active.task_path)
if not task_dir.is_absolute():
task_dir = root / task_dir
if active.stale:
return task_dir.name, f"stale_{active.source_type}", active.source
task_json = task_dir / "task.json"
if not task_json.is_file():
return None
try:
data = json.loads(task_json.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
task_id = data.get("id") or task_dir.name
status = data.get("status", "")
if not isinstance(status, str) or not status:
return None
return task_id, status, active.source
# ---------------------------------------------------------------------------
# Breadcrumb loading: parse workflow.md, fall back to hardcoded defaults
# ---------------------------------------------------------------------------
# Supports STATUS values with letters, digits, underscores, hyphens
# (so "in-review" / "blocked-by-team" work alongside "in_progress").
_TAG_RE = re.compile(
r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n(.*?)\n\s*\[/workflow-state:\1\]",
re.DOTALL,
)
# Hardcoded defaults for built-in Trellis statuses. Used when workflow.md is
# missing, malformed, or lacks the tag for this status.
#
# `no_task` is a pseudo-status emitted when no session active task exists — it keeps
# the Next-Action reminder flowing per-turn even without an active task.
_FALLBACK_BREADCRUMBS = {
"no_task": (
"No active task.\n"
"Trigger words in the user message that suggest creating a task: "
"重构 / 抽成 / 独立 / 分发 / 拆出来 / 搞一个 / 做成 / 接入 / 集成 / "
"refactor / rewrite / extract / productize / publish / build X / design Y.\n"
"Task is NOT required if ALL three hold: (a) zero file writes this turn, "
"(b) answer fits in one reply with no multi-round plan, (c) no research "
"beyond reading 1-2 repo files.\n"
"When in doubt and no override below applies: prefer creating a task — "
"over-tasking is cheap; under-tasking leaks plans and research into "
"main context.\n"
"Flow: load `trellis-brainstorm` skill → it creates the task via "
"`python3 ./.trellis/scripts/task.py create` and drives requirements Q&A. "
"For research-heavy work (tool comparison, docs, cross-platform survey), "
"spawn `trellis-research` sub-agents via Task tool — NEVER do 3+ inline "
"WebFetch/WebSearch/`gh api` calls in the main conversation.\n"
"User override (per-turn escape hatch): if the user's CURRENT message "
"contains an explicit opt-out phrase (\"跳过 trellis\" / \"别走流程\" / "
"\"小修一下\" / \"直接改\" / \"先别建任务\" / \"skip trellis\" / "
"\"no task\" / \"just do it\" / \"don't create a task\"), honor it for "
"this turn — briefly acknowledge (\"好,本轮跳过 trellis 流程\") and "
"proceed without creating a task. Per-turn only; does not carry forward; "
"do NOT invent an override the user did not say."
),
"planning": (
"Complete prd.md via trellis-brainstorm skill; then run task.py start.\n"
"Research belongs in `{task_dir}/research/*.md`, written by "
"`trellis-research` sub-agents. Do NOT inline WebFetch/WebSearch in "
"main session — PRD only links to research files."
),
"in_progress": (
"Flow: trellis-implement → trellis-check → trellis-update-spec → finish\n"
"Next required action: inspect conversation history + git status, then "
"execute the next uncompleted step in that sequence.\n"
"For agent-capable platforms, the default is to dispatch "
"`trellis-implement` for implementation and `trellis-check` before "
"reporting completion — do not edit code in the main session by default.\n"
"Use the exact Trellis agent type names when spawning sub-agents: "
"`trellis-implement`, `trellis-check`, or `trellis-research`. "
"Generic/default/generalPurpose sub-agents do not receive "
"`implement.jsonl` / `check.jsonl` injection.\n"
"User override (per-turn escape hatch): if the user's CURRENT message "
"explicitly tells the main session to handle it directly (\"你直接改\" / "
"\"别派 sub-agent\" / \"main session 写就行\" / \"do it inline\" / "
"\"不用 sub-agent\"), honor it for this turn and edit code directly. "
"Per-turn only; does not carry forward; do NOT invent an override the "
"user did not say."
),
"completed": (
"Code committed via Phase 3.4; run `/trellis:finish-work` to wrap up "
"(archive task + record session).\n"
"If you reach this state with uncommitted code, return to Phase 3.4 "
"first — `/finish-work` refuses to run on a dirty working tree.\n"
"`task.py archive` deletes runtime session files that point at the "
"archived task."
),
}
def load_breadcrumbs(root: Path) -> dict[str, str]:
"""Parse workflow.md for [workflow-state:STATUS] blocks.
Returns {status: body_text}. Missing tags fall back to hardcoded
defaults so the hook always has something to say for built-in
statuses. Custom statuses without tags fall to generic breadcrumb
downstream (see build_breadcrumb).
"""
result = dict(_FALLBACK_BREADCRUMBS)
workflow = root / ".trellis" / "workflow.md"
if not workflow.is_file():
return result
try:
content = workflow.read_text(encoding="utf-8")
except OSError:
return result
for match in _TAG_RE.finditer(content):
status = match.group(1)
body = match.group(2).strip()
if body:
result[status] = body
return result
def build_breadcrumb(
task_id: Optional[str],
status: str,
templates: dict[str, str],
source: str | None = None,
) -> str:
"""Build the <workflow-state>...</workflow-state> block.
- Known status (in templates or fallback) → detailed template body
- Unknown status (no tag + no fallback) → generic "refer to workflow.md"
- `no_task` pseudo-status (task_id is None) → header omits task info
"""
body = templates.get(status)
if body is None:
body = "Refer to workflow.md for current step."
header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})"
if source:
header = f"{header}\nSource: {source}"
return f"<workflow-state>\n{header}\n{body}\n</workflow-state>"
# ---------------------------------------------------------------------------
# Entry
# ---------------------------------------------------------------------------
def main() -> int:
try:
data = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
data = {}
cwd_str = data.get("cwd") or os.getcwd()
cwd = Path(cwd_str)
root = find_trellis_root(cwd)
if root is None:
return 0 # not a Trellis project
templates = load_breadcrumbs(root)
task = get_active_task(root, data)
if task is None:
# No active task — still emit a breadcrumb nudging AI toward
# trellis-brainstorm + task.py create when user describes real work.
breadcrumb = build_breadcrumb(None, "no_task", templates)
else:
task_id, status, source = task
breadcrumb = build_breadcrumb(task_id, status, templates, source)
output = {
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": breadcrumb,
}
}
print(json.dumps(output))
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,362 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Codex Session Start Hook - Inject Trellis context into Codex sessions.
Output format follows Codex hook protocol:
stdout JSON → { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: "..." } }
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import warnings
from io import StringIO
from pathlib import Path
warnings.filterwarnings("ignore")
FIRST_REPLY_NOTICE = """<first-reply-notice>
On the first visible assistant reply in this session, begin with exactly one short Chinese sentence:
Trellis SessionStart 已注入:workflow、当前任务状态、开发者身份、git 状态、active tasks、spec 索引已加载。
Then continue directly with the user's request. This notice is one-shot: do not repeat it after the first assistant reply in the same session.
</first-reply-notice>"""
def should_skip_injection() -> bool:
return os.environ.get("CODEX_NON_INTERACTIVE") == "1"
def configure_project_encoding(project_dir: Path) -> None:
"""Reuse Trellis' shared Windows stdio encoding helper before JSON output."""
scripts_dir = project_dir / ".trellis" / "scripts"
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
try:
from common import configure_encoding # type: ignore[import-not-found]
configure_encoding()
except Exception:
pass
def _has_curated_jsonl_entry(jsonl_path: Path) -> bool:
"""Return True iff jsonl has at least one row with a ``file`` field.
A freshly seeded jsonl only contains a ``{"_example": ...}`` row (no
``file`` key) — that is NOT "ready". Readiness requires at least one
curated entry. Matches the contract used by ``inject-subagent-context.py``.
"""
try:
for line in jsonl_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
row = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(row, dict) and row.get("file"):
return True
except (OSError, UnicodeDecodeError):
return False
return False
def read_file(path: Path, fallback: str = "") -> str:
try:
return path.read_text(encoding="utf-8")
except (FileNotFoundError, PermissionError):
return fallback
def _resolve_context_key(project_dir: Path, hook_input: dict) -> str | None:
scripts_dir = project_dir / ".trellis" / "scripts"
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
try:
from common.active_task import resolve_context_key # type: ignore[import-not-found]
except Exception:
return None
return resolve_context_key(hook_input, platform="codex")
def _resolve_active_task(trellis_dir: Path, hook_input: dict):
scripts_dir = trellis_dir / "scripts"
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
from common.active_task import resolve_active_task # type: ignore[import-not-found]
return resolve_active_task(trellis_dir.parent, hook_input, platform="codex")
def run_script(script_path: Path, context_key: str | None = None) -> str:
try:
env = os.environ.copy()
env["PYTHONIOENCODING"] = "utf-8"
if context_key:
env["TRELLIS_CONTEXT_ID"] = context_key
cmd = [sys.executable, "-W", "ignore", str(script_path)]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
cwd=str(script_path.parent.parent.parent),
env=env,
)
return result.stdout if result.returncode == 0 else "No context available"
except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError):
return "No context available"
def _normalize_task_ref(task_ref: str) -> str:
normalized = task_ref.strip()
if not normalized:
return ""
path_obj = Path(normalized)
if path_obj.is_absolute():
return str(path_obj)
normalized = normalized.replace("\\", "/")
while normalized.startswith("./"):
normalized = normalized[2:]
if normalized.startswith("tasks/"):
return f".trellis/{normalized}"
return normalized
def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path:
normalized = _normalize_task_ref(task_ref)
path_obj = Path(normalized)
if path_obj.is_absolute():
return path_obj
if normalized.startswith(".trellis/"):
return trellis_dir.parent / path_obj
return trellis_dir / "tasks" / path_obj
def _get_task_status(trellis_dir: Path, hook_input: dict) -> str:
active = _resolve_active_task(trellis_dir, hook_input)
if not active.task_path:
return f"Status: NO ACTIVE TASK\nSource: {active.source}\nNext: Describe what you want to work on"
task_ref = active.task_path
task_dir = _resolve_task_dir(trellis_dir, task_ref)
if active.stale or not task_dir.is_dir():
return f"Status: STALE POINTER\nTask: {task_ref}\nSource: {active.source}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish"
task_json_path = task_dir / "task.json"
task_data: dict = {}
if task_json_path.is_file():
try:
task_data = json.loads(task_json_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, PermissionError):
pass
task_title = task_data.get("title", task_ref)
task_status = task_data.get("status", "unknown")
if task_status == "completed":
return f"Status: COMPLETED\nTask: {task_title}\nSource: {active.source}\nNext: Archive with `python3 ./.trellis/scripts/task.py archive {task_dir.name}` or start a new task"
has_context = False
for jsonl_name in ("implement.jsonl", "check.jsonl", "spec.jsonl"):
jsonl_path = task_dir / jsonl_name
if jsonl_path.is_file() and _has_curated_jsonl_entry(jsonl_path):
has_context = True
break
has_prd = (task_dir / "prd.md").is_file()
if not has_prd:
return f"Status: NOT READY\nTask: {task_title}\nSource: {active.source}\nMissing: prd.md not created\nNext: Write PRD (see workflow.md Phase 1.1) then curate implement.jsonl per Phase 1.3"
if not has_context:
return f"Status: NOT READY\nTask: {task_title}\nSource: {active.source}\nMissing: implement.jsonl / check.jsonl missing or empty\nNext: Curate entries per workflow.md Phase 1.3 (spec + research files only), then `task.py start`"
return (
f"Status: READY\nTask: {task_title}\n"
f"Source: {active.source}\n"
"Next required action: dispatch `trellis-implement` per Phase 2.1. "
"For agent-capable platforms, the default is to NOT edit code in the main session. "
"After implementation, dispatch `trellis-check` per Phase 2.2 before reporting completion.\n"
"User override (per-turn escape hatch): if the user's CURRENT message explicitly tells the "
"main session to handle it directly (\"你直接改\" / \"别派 sub-agent\" / \"main session 写就行\" / "
"\"do it inline\" / \"不用 sub-agent\"), honor it for this turn and edit code directly. "
"Per-turn only; do NOT invent an override the user did not say."
)
def _extract_range(content: str, start_header: str, end_header: str) -> str:
"""Extract lines starting at `## start_header` up to (but excluding) `## end_header`."""
lines = content.splitlines()
start: "int | None" = None
end: int = len(lines)
start_match = f"## {start_header}"
end_match = f"## {end_header}"
for i, line in enumerate(lines):
stripped = line.strip()
if start is None and stripped == start_match:
start = i
continue
if start is not None and stripped == end_match:
end = i
break
if start is None:
return ""
return "\n".join(lines[start:end]).rstrip()
def _build_workflow_toc(workflow_path: Path) -> str:
"""Inject workflow guide: TOC + Phase Index + Phase 1/2/3 step details."""
content = read_file(workflow_path)
if not content:
return "No workflow.md found"
out_lines = [
"# Development Workflow — Section Index",
"Full guide: .trellis/workflow.md (read on demand)",
"",
"## Table of Contents",
]
for line in content.splitlines():
if line.startswith("## "):
out_lines.append(line)
out_lines += ["", "---", ""]
phases = _extract_range(content, "Phase Index", "Workflow State Breadcrumbs")
if phases:
out_lines.append(phases)
return "\n".join(out_lines).rstrip()
def main() -> None:
if should_skip_injection():
sys.exit(0)
# Read hook input from stdin
try:
hook_input = json.loads(sys.stdin.read())
if not isinstance(hook_input, dict):
hook_input = {}
project_dir = Path(hook_input.get("cwd", ".")).resolve()
except (json.JSONDecodeError, KeyError):
hook_input = {}
project_dir = Path(".").resolve()
configure_project_encoding(project_dir)
trellis_dir = project_dir / ".trellis"
context_key = _resolve_context_key(project_dir, hook_input)
output = StringIO()
output.write("""<session-context>
You are starting a new session in a Trellis-managed project.
Read and follow all instructions below carefully.
</session-context>
""")
output.write(FIRST_REPLY_NOTICE)
output.write("\n\n")
output.write("<current-state>\n")
context_script = trellis_dir / "scripts" / "get_context.py"
output.write(run_script(context_script, context_key))
output.write("\n</current-state>\n\n")
output.write("<workflow>\n")
output.write(_build_workflow_toc(trellis_dir / "workflow.md"))
output.write("\n</workflow>\n\n")
output.write("<guidelines>\n")
output.write(
"Project spec indexes are listed by path below. Each index contains a "
"**Pre-Development Checklist** listing the specific guideline files to "
"read before coding.\n\n"
"- If you're spawning an implement/check sub-agent, context is injected "
"automatically via `{task}/implement.jsonl` / `check.jsonl`. You do NOT "
"need to read these indexes yourself.\n"
"- For agent-capable platforms, the default is to dispatch "
"`trellis-implement` and `trellis-check` (so JSONL context is loaded by "
"the sub-agents) rather than editing code in the main session. "
"Honor a per-turn user override only if the user's current message "
"explicitly opts out (see <task-status> below for override phrases).\n\n"
)
# guides/ inlined (cross-package thinking, broadly useful)
guides_index = trellis_dir / "spec" / "guides" / "index.md"
if guides_index.is_file():
output.write("## guides (inlined — cross-package thinking guides)\n")
output.write(read_file(guides_index))
output.write("\n\n")
# Other indexes — paths only
paths: list[str] = []
spec_dir = trellis_dir / "spec"
if spec_dir.is_dir():
for sub in sorted(spec_dir.iterdir()):
if not sub.is_dir() or sub.name.startswith("."):
continue
if sub.name == "guides":
continue
index_file = sub / "index.md"
if index_file.is_file():
paths.append(f".trellis/spec/{sub.name}/index.md")
else:
for nested in sorted(sub.iterdir()):
if not nested.is_dir():
continue
nested_index = nested / "index.md"
if nested_index.is_file():
paths.append(
f".trellis/spec/{sub.name}/{nested.name}/index.md"
)
if paths:
output.write("## Available spec indexes (read on demand)\n")
for p in paths:
output.write(f"- {p}\n")
output.write("\n")
output.write(
"Discover more via: "
"`python3 ./.trellis/scripts/get_context.py --mode packages`\n"
)
output.write("</guidelines>\n\n")
task_status = _get_task_status(trellis_dir, hook_input)
output.write(f"<task-status>\n{task_status}\n</task-status>\n\n")
output.write("""<ready>
Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them.
When the user sends the first message, follow <task-status> and the workflow guide.
If a task is READY, execute its Next required action without asking whether to continue.
</ready>""")
context = output.getvalue()
result = {
"suppressOutput": True,
"systemMessage": f"Trellis context injected ({len(context)} chars)",
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": context,
},
}
print(json.dumps(result, ensure_ascii=False), flush=True)
if __name__ == "__main__":
main()