This commit is contained in:
zhu
2026-05-07 16:14:31 +08:00
parent 95e99ec802
commit c661f4a063
151 changed files with 22280 additions and 4717 deletions

View File

@@ -0,0 +1,109 @@
---
name: trellis-check
description: |
Code quality check expert. Reviews code changes against specs and self-fixes issues.
tools: Read, Write, Edit, Bash, Glob, Grep, mcp__exa__web_search_exa, mcp__exa__get_code_context_exa
---
# Check Agent
You are the Check Agent in the Trellis workflow.
## Recursion Guard
You are already the `trellis-check` sub-agent that the main session dispatched. Do the review and fixes directly.
- Do NOT spawn another `trellis-check` or `trellis-implement` sub-agent.
- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role.
- Only the main session may dispatch Trellis implement/check agents. If more implementation work is needed, report that recommendation instead of spawning.
## Trellis Context Loading Protocol
Look for the `<!-- trellis-hook-injected -->` marker in your input above.
- **If the marker is present**: prd / spec / research files have already been auto-loaded for you above. Proceed with the check work directly.
- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: <path>`, then Read `<task-path>/prd.md` and the spec files listed in `<task-path>/check.jsonl` yourself before doing the work.
## Context
Before checking, read:
- `.trellis/spec/` - Development guidelines
- Pre-commit checklist for quality standards
## Core Responsibilities
1. **Get code changes** - Use git diff to get uncommitted code
2. **Check against specs** - Verify code follows guidelines
3. **Self-fix** - Fix issues yourself, not just report them
4. **Run verification** - typecheck and lint
## Important
**Fix issues yourself**, don't just report them.
You have write and edit tools, you can modify code directly.
---
## Workflow
### Step 1: Get Changes
```bash
git diff --name-only # List changed files
git diff # View specific changes
```
### Step 2: Check Against Specs
Read relevant specs in `.trellis/spec/` to check code:
- Does it follow directory structure conventions
- Does it follow naming conventions
- Does it follow code patterns
- Are there missing types
- Are there potential bugs
### Step 3: Self-Fix
After finding issues:
1. Fix the issue directly (use edit tool)
2. Record what was fixed
3. Continue checking other issues
### Step 4: Run Verification
Run project's lint and typecheck commands to verify changes.
If failed, fix issues and re-run.
---
## Report Format
```markdown
## Self-Check Complete
### Files Checked
- src/components/Feature.tsx
- src/hooks/useFeature.ts
### Issues Found and Fixed
1. `<file>:<line>` - <what was fixed>
2. `<file>:<line>` - <what was fixed>
### Issues Not Fixed
(If there are issues that cannot be self-fixed, list them here with reasons)
### Verification Results
- TypeCheck: Passed
- Lint: Passed
### Summary
Checked X files, found Y issues, all fixed.
```

View File

@@ -0,0 +1,109 @@
---
name: trellis-implement
description: |
Code implementation expert. Understands specs and requirements, then implements features. No git commit allowed.
tools: Read, Write, Edit, Bash, Glob, Grep, mcp__exa__web_search_exa, mcp__exa__get_code_context_exa
---
# Implement Agent
You are the Implement Agent in the Trellis workflow.
## Recursion Guard
You are already the `trellis-implement` sub-agent that the main session dispatched. Do the implementation work directly.
- Do NOT spawn another `trellis-implement` or `trellis-check` sub-agent.
- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role.
- Only the main session may dispatch Trellis implement/check agents. If more parallel work is needed, report that recommendation instead of spawning.
## Trellis Context Loading Protocol
Look for the `<!-- trellis-hook-injected -->` marker in your input above.
- **If the marker is present**: prd / spec / research files have already been auto-loaded for you above. Proceed with the implementation work directly.
- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: <path>`, then Read `<task-path>/prd.md`, `<task-path>/info.md` (if it exists), and the spec files listed in `<task-path>/implement.jsonl` yourself before doing the work.
## Context
Before implementing, read:
- `.trellis/workflow.md` - Project workflow
- `.trellis/spec/` - Development guidelines
- Task `prd.md` - Requirements document
- Task `info.md` - Technical design (if exists)
## Core Responsibilities
1. **Understand specs** - Read relevant spec files in `.trellis/spec/`
2. **Understand requirements** - Read prd.md and info.md
3. **Implement features** - Write code following specs and design
4. **Self-check** - Ensure code quality
5. **Report results** - Report completion status
## Forbidden Operations
**Do NOT execute these git commands:**
- `git commit`
- `git push`
- `git merge`
---
## Workflow
### 1. Understand Specs
Read relevant specs based on task type:
- Spec layers: `.trellis/spec/<package>/<layer>/`
- Shared guides: `.trellis/spec/guides/`
### 2. Understand Requirements
Read the task's prd.md and info.md:
- What are the core requirements
- Key points of technical design
- Which files to modify/create
### 3. Implement Features
- Write code following specs and technical design
- Follow existing code patterns
- Only do what's required, no over-engineering
### 4. Verify
Run project's lint and typecheck commands to verify changes.
---
## Report Format
```markdown
## Implementation Complete
### Files Modified
- `src/components/Feature.tsx` - New component
- `src/hooks/useFeature.ts` - New hook
### Implementation Summary
1. Created Feature component...
2. Added useFeature hook...
### Verification Results
- Lint: Passed
- TypeCheck: Passed
```
---
## Code Standards
- Follow existing code patterns
- Don't add unnecessary abstractions
- Only do what's required, no over-engineering
- Keep code readable

View File

@@ -0,0 +1,137 @@
---
name: trellis-research
description: |
Code and tech search expert. Finds files, patterns, and tech solutions, and PERSISTS every finding to the current task's research/ directory. No code modifications outside that directory.
tools: Read, Write, Glob, Grep, Bash, mcp__exa__web_search_exa, mcp__exa__get_code_context_exa, Skill, mcp__chrome-devtools__*
---
# Research Agent
You are the Research Agent in the Trellis workflow.
## Core Principle
**You do one thing: find, explain, and PERSIST information.**
Conversations get compacted; files don't. Every research output MUST end up as a file under `{TASK_DIR}/research/`. Returning findings only through the chat reply is a failure — the caller cannot read them next session.
---
## Core Responsibilities
1. **Internal Search** — locate files/components, understand code logic, discover patterns (Glob, Grep, Read)
2. **External Search** — library docs, API references, best practices (web search)
3. **Persist** — write each research topic to `{TASK_DIR}/research/<topic>.md`
4. **Report** — return file paths + one-line summaries to the main agent (not full content)
---
## Workflow
### Step 1: Resolve Current Task
Run `python ./.trellis/scripts/task.py current --source` → active task path. If no active task is set, ask the user where to write output; do NOT guess.
Ensure `{TASK_DIR}/research/` exists:
```bash
mkdir -p <TASK_DIR>/research
```
### Step 2: Understand Search Request
Classify: internal / external / mixed. Determine scope (global / specific directory) and expected shape (file list / pattern notes / tech comparison).
### Step 3: Execute Search
Run independent searches in parallel (Glob + Grep + web) for efficiency.
### Step 4: Persist Each Topic
For each distinct research topic, Write a markdown file at `{TASK_DIR}/research/<topic-slug>.md`. Use the File Format below.
### Step 5: Report to Main Agent
Reply with ONLY:
- List of files written (paths relative to repo root)
- One-line summary per file
- Any critical caveats that the main agent needs to know right now
Do NOT paste full research content into the reply. The files are the contract.
---
## Scope Limits (Strict)
### Write ALLOWED
- `{TASK_DIR}/research/*.md` — your own output
- Creating `{TASK_DIR}/research/` if it doesn't exist (via `mkdir -p`)
### Write FORBIDDEN
- Code files (`src/`, `lib/`, …)
- Spec files (`.trellis/spec/`) — main agent should use `update-spec` skill instead
- `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.claude/`, `.cursor/`, etc.)
- Other task directories
- Any git operation (commit / push / branch / merge)
If the user asks you to edit code, decline and suggest spawning `implement` instead.
---
## File Format
Each `{TASK_DIR}/research/<topic>.md` should follow:
```markdown
# Research: <topic>
- **Query**: <original query>
- **Scope**: <internal / external / mixed>
- **Date**: <YYYY-MM-DD>
## Findings
### Files Found
| File Path | Description |
|---|---|
| `src/services/xxx.ts` | Main implementation |
| `src/types/xxx.ts` | Type definitions |
### Code Patterns
<describe patterns, cite file:line>
### External References
- [Library X docs](url) — <why relevant, version constraints>
### Related Specs
- `.trellis/spec/xxx.md` — <description>
## Caveats / Not Found
<anything incomplete or uncertain>
```
---
## Guidelines
### DO
- Provide specific file paths and line numbers
- Quote actual code snippets
- Persist every topic to its own file
- Return file paths in your reply, not the full content
- Mark "not found" explicitly when searches come up empty
### DON'T
- Don't write code or modify files outside `{TASK_DIR}/research/`
- Don't guess uncertain info
- Don't paste full research text into the reply (files are the deliverable)
- Don't propose improvements or critique implementation (that's not your role)

View File

@@ -0,0 +1,55 @@
# Continue Current Task
Resume work on the current task — pick up at the right phase/step in `.trellis/workflow.md`.
---
## Step 1: Load Current Context
```bash
python ./.trellis/scripts/get_context.py
```
Confirms: current task, git state, recent commits.
## Step 2: Load the Phase Index
```bash
python ./.trellis/scripts/get_context.py --mode phase
```
Shows the Phase Index (Plan / Execute / Finish) with routing + skill mapping.
## Step 3: Decide Where You Are
`get_context.py` shows the active task's `status` field. Route by `status` + artifact presence:
- `status=planning` + no `prd.md`**1.1** (load `trellis-brainstorm`)
- `status=planning` + `prd.md` exists + `implement.jsonl` not curated (only the seed `_example` row) → **1.3**
- `status=planning` + `prd.md` + curated `implement.jsonl`**1.4** (run `task.py start` to enter Phase 2)
- `status=in_progress` + implementation not started → **2.1**
- `status=in_progress` + implementation done, not yet checked → **2.2**
- `status=in_progress` + check passed → **3.1**
- `status=completed` (rare; usually archived immediately) → archive flow
Phase rules (full detail in `.trellis/workflow.md`):
1. Run steps **in order** within a phase — `[required]` steps must not be skipped
2. `[once]` steps are already done if the output exists (e.g., `prd.md` for 1.1; `implement.jsonl` with curated entries for 1.3) — skip them
3. You may go back to an earlier phase if discoveries require it
## Step 4: Load the Specific Step
Once you know which step to resume at:
```bash
python ./.trellis/scripts/get_context.py --mode phase --step <X.X> --platform claude
```
Follow the loaded instructions. After each `[required]` step completes, move to the next.
---
## Reference
Full workflow, skill routing table, and the DO-NOT-skip table live in `.trellis/workflow.md`. This command is only an entry point — the canonical guidance is there.

View File

@@ -0,0 +1,66 @@
# Finish Work
Wrap up the current session: archive the active task (and any other completed-but-unarchived tasks the user wants to clean up) and record the session journal. Code commits are NOT done here — those happen in workflow Phase 3.4 before you invoke this command.
## Step 1: Survey current state
```bash
python ./.trellis/scripts/get_context.py --mode record
```
This prints:
- **My active tasks** — review whether any besides the current one are actually done (code merged, AC met) and should be archived this round.
- **Git status** — quick visual on what's dirty.
- **Recent commits** — you'll need their hashes in Step 4 for `--commit`.
If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless.
## Step 2: Sanity check — classify dirty paths
Run:
```bash
git status --porcelain
```
Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this skill's own work.
For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics:
- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task
- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task
- Paths in unrelated areas you have no recollection of touching this session → other parallel work
Then route:
- **Any remaining path looks like current-task work** — bail out with:
> "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running `/trellis:finish-work`."
Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there.
- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3:
> "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`."
- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer.
## Step 3: Archive task(s)
```bash
python ./.trellis/scripts/task.py archive <task-name>
```
At minimum: the current active task (if any). Plus any extra tasks the user confirmed in Step 1. Each archive produces a `chore(task): archive ...` commit via the script's auto-commit.
If there is no active task and the user did not confirm any cleanup archives, skip this step.
## Step 4: Record session journal
```bash
python ./.trellis/scripts/add_session.py \
--title "Session Title" \
--commit "hash1,hash2" \
--summary "Brief summary"
```
Use the work-commit hashes produced in Phase 3.4 (visible in Step 1's `Recent commits` list, or via `git log --oneline`) for `--commit`. Do not include the archive commit hashes from Step 3. This produces a `chore: record journal` commit.
Final git log order: `<work commits from 3.4>``chore(task): archive ...` (one or more) → `chore: record journal`.

View File

@@ -0,0 +1,749 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Multi-Platform Sub-Agent Context Injection Hook
Injects task-specific context when sub-agents (implement, check, research) are spawned.
Core Design Philosophy:
- Hook is responsible for injecting all context, subagent works autonomously with complete info
- Each agent has a dedicated jsonl file defining its context
- No resume needed, no segmentation, behavior controlled by code not prompt
Trigger: PreToolUse (before Task tool call)
Context Source: Trellis active task resolver points to task directory
- implement.jsonl - Implement agent dedicated context
- check.jsonl - Check agent dedicated context
- prd.md - Requirements document
- info.md - Technical design
- codex-review-output.txt - Code Review results
"""
from __future__ import annotations
# IMPORTANT: Suppress all warnings FIRST
import warnings
warnings.filterwarnings("ignore")
import json
import os
import sys
from pathlib import Path
from typing import Any
# IMPORTANT: Force stdout to use UTF-8 on Windows
# This fixes UnicodeEncodeError when outputting non-ASCII characters
if sys.platform.startswith("win"):
import io as _io
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
elif hasattr(sys.stdout, "detach"):
sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr]
# =============================================================================
# Path Constants (change here to rename directories)
# =============================================================================
DIR_WORKFLOW = ".trellis"
DIR_SPEC = "spec"
FILE_TASK_JSON = "task.json"
# =============================================================================
# Subagent Constants (change here to rename subagent types)
# =============================================================================
AGENT_IMPLEMENT = "trellis-implement"
AGENT_CHECK = "trellis-check"
AGENT_RESEARCH = "trellis-research"
# Agents that require a task directory
AGENTS_REQUIRE_TASK = (AGENT_IMPLEMENT, AGENT_CHECK)
# All supported agents
AGENTS_ALL = (AGENT_IMPLEMENT, AGENT_CHECK, AGENT_RESEARCH)
def find_repo_root(start_path: str) -> str | None:
"""
Find git repo root from start_path upwards
Returns:
Repo root path, or None if not found
"""
current = Path(start_path).resolve()
while current != current.parent:
if (current / ".git").exists():
return str(current)
current = current.parent
return None
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 ".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 get_current_task(repo_root: str, input_data: dict) -> str | None:
"""Resolve current task directory through the unified active task resolver."""
scripts_dir = Path(repo_root) / DIR_WORKFLOW / "scripts"
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
try:
from common.active_task import resolve_active_task # type: ignore[import-not-found]
except Exception:
return None
active = resolve_active_task(
Path(repo_root),
input_data,
platform=_detect_platform(input_data),
)
return active.task_path
def read_file_content(base_path: str, file_path: str) -> str | None:
"""Read file content, return None if file doesn't exist"""
full_path = os.path.join(base_path, file_path)
if os.path.exists(full_path) and os.path.isfile(full_path):
try:
with open(full_path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return None
return None
def read_directory_contents(
base_path: str, dir_path: str, max_files: int = 20
) -> list[tuple[str, str]]:
"""
Read all .md files in a directory
Args:
base_path: Base path (usually repo_root)
dir_path: Directory relative path
max_files: Max files to read (prevent huge directories)
Returns:
[(file_path, content), ...]
"""
full_path = os.path.join(base_path, dir_path)
if not os.path.exists(full_path) or not os.path.isdir(full_path):
return []
results = []
try:
# Only read .md files, sorted by filename
md_files = sorted(
[
f
for f in os.listdir(full_path)
if f.endswith(".md") and os.path.isfile(os.path.join(full_path, f))
]
)
for filename in md_files[:max_files]:
file_full_path = os.path.join(full_path, filename)
relative_path = os.path.join(dir_path, filename)
try:
with open(file_full_path, "r", encoding="utf-8") as f:
content = f.read()
results.append((relative_path, content))
except Exception:
continue
except Exception:
pass
return results
def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]:
"""
Read all file/directory contents referenced in jsonl file
Schema:
{"file": "path/to/file.md", "reason": "..."}
{"file": "path/to/dir/", "type": "directory", "reason": "..."}
{"_example": "..."} # seed row — 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.
Returns:
[(path, content), ...]
"""
full_path = os.path.join(base_path, jsonl_path)
if not os.path.exists(full_path):
print(
f"[inject-subagent-context] WARN: {jsonl_path} not found — "
f"sub-agent will receive only prd.md",
file=sys.stderr,
)
return []
results = []
saw_real_entry = False
try:
with open(full_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
item = json.loads(line)
file_path = item.get("file") or item.get("path")
entry_type = item.get("type", "file")
if not file_path:
# Seed / comment row — skip silently
continue
saw_real_entry = True
if entry_type == "directory":
# Read all .md files in directory
dir_contents = read_directory_contents(base_path, file_path)
results.extend(dir_contents)
else:
# Read single file
content = read_file_content(base_path, file_path)
if content:
results.append((file_path, content))
except json.JSONDecodeError:
continue
except Exception:
pass
if not saw_real_entry:
print(
f"[inject-subagent-context] WARN: {jsonl_path} has no curated "
f"entries (only seed / empty) — sub-agent will receive only "
f"prd.md. See workflow.md Phase 1.3 for curation guidance.",
file=sys.stderr,
)
return results
def get_agent_context(repo_root: str, task_dir: str, agent_type: str) -> str:
"""
Get context from {agent_type}.jsonl for the specified agent.
Only reads implement.jsonl or check.jsonl (the two JSONL files the task system creates).
"""
context_parts = []
agent_jsonl = f"{task_dir}/{agent_type}.jsonl"
for file_path, content in read_jsonl_entries(repo_root, agent_jsonl):
context_parts.append(f"=== {file_path} ===\n{content}")
return "\n\n".join(context_parts)
def get_implement_context(repo_root: str, task_dir: str) -> str:
"""
Complete context for Implement Agent
Read order:
1. All files in implement.jsonl (dev specs)
2. prd.md (requirements)
3. info.md (technical design)
"""
context_parts = []
# 1. Read implement.jsonl
base_context = get_agent_context(repo_root, task_dir, "implement")
if base_context:
context_parts.append(base_context)
# 2. Requirements document
prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
if prd_content:
context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
# 3. Technical design
info_content = read_file_content(repo_root, f"{task_dir}/info.md")
if info_content:
context_parts.append(
f"=== {task_dir}/info.md (Technical Design) ===\n{info_content}"
)
return "\n\n".join(context_parts)
def get_check_context(repo_root: str, task_dir: str) -> str:
"""
Context for Check Agent: check.jsonl + prd.md
"""
context_parts = []
for file_path, content in read_jsonl_entries(repo_root, f"{task_dir}/check.jsonl"):
context_parts.append(f"=== {file_path} ===\n{content}")
prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
if prd_content:
context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
return "\n\n".join(context_parts)
def get_finish_context(repo_root: str, task_dir: str) -> str:
"""
Context for Finish phase: reuses check.jsonl + prd.md
(Finish is a final check, same context source.)
"""
return get_check_context(repo_root, task_dir)
def build_implement_prompt(original_prompt: str, context: str) -> str:
"""Build complete prompt for Implement"""
return f"""<!-- trellis-hook-injected -->
# Implement Agent Task
You are the Implement Agent in the Multi-Agent Pipeline.
## Your Context
All the information you need has been prepared for you:
{context}
---
## Your Task
{original_prompt}
---
## Workflow
1. **Understand specs** - All dev specs are injected above, understand them
2. **Understand requirements** - Read requirements document and technical design
3. **Implement feature** - Implement following specs and design
4. **Self-check** - Ensure code quality against check specs
## Important Constraints
- Do NOT execute git commit, only code modifications
- Follow all dev specs injected above
- Report list of modified/created files when done"""
def build_check_prompt(original_prompt: str, context: str) -> str:
"""Build complete prompt for Check"""
return f"""<!-- trellis-hook-injected -->
# Check Agent Task
You are the Check Agent in the Multi-Agent Pipeline (code and cross-layer checker).
## Your Context
All check specs and dev specs you need:
{context}
---
## Your Task
{original_prompt}
---
## Workflow
1. **Get changes** - Run `git diff --name-only` and `git diff` to get code changes
2. **Check against specs** - Check item by item against specs above
3. **Self-fix** - Fix issues directly, don't just report
4. **Run verification** - Run project's lint and typecheck commands
## Important Constraints
- Fix issues yourself, don't just report
- Must execute complete checklist in check specs
- Pay special attention to impact radius analysis (L1-L5)"""
def build_finish_prompt(original_prompt: str, context: str) -> str:
"""Build complete prompt for Finish (final check before PR)"""
return f"""<!-- trellis-hook-injected -->
# Finish Agent Task
You are performing the final check before creating a PR.
## Your Context
Finish checklist and requirements:
{context}
---
## Your Task
{original_prompt}
---
## Workflow
1. **Review changes** - Run `git diff --name-only` to see all changed files
2. **Verify requirements** - Check each requirement in prd.md is implemented
3. **Spec sync** - Analyze whether changes introduce new patterns, contracts, or conventions
- If new pattern/convention found: read target spec file → update it → update index.md if needed
- If infra/cross-layer change: follow the 7-section mandatory template from update-spec.md
- If pure code fix with no new patterns: skip this step
4. **Run final checks** - Execute lint and typecheck
5. **Confirm ready** - Ensure code is ready for PR
## Important Constraints
- You MAY update spec files when gaps are detected (use update-spec.md as guide)
- MUST read the target spec file BEFORE editing (avoid duplicating existing content)
- Do NOT update specs for trivial changes (typos, formatting, obvious fixes)
- If critical CODE issues found, report them clearly (fix specs, not code)
- Verify all acceptance criteria in prd.md are met"""
def get_research_context(repo_root: str, task_dir: str | None) -> str:
"""
Context for Research Agent — project structure overview for spec directories.
`task_dir` kept for signature parity with get_implement_context / get_check_context
so the dispatcher can call them uniformly.
"""
_ = task_dir
context_parts = []
# 1. Project structure overview (dynamically discover spec directories)
spec_path = f"{DIR_WORKFLOW}/{DIR_SPEC}"
spec_root = Path(repo_root) / DIR_WORKFLOW / DIR_SPEC
# Build spec tree dynamically
tree_lines = [f"{spec_path}/"]
if spec_root.is_dir():
pkg_dirs = sorted(d for d in spec_root.iterdir() if d.is_dir())
for i, pkg_dir in enumerate(pkg_dirs):
is_last = i == len(pkg_dirs) - 1
prefix = "└── " if is_last else "├── "
layers = sorted(d.name for d in pkg_dir.iterdir() if d.is_dir())
layer_info = f" ({', '.join(layers)})" if layers else ""
tree_lines.append(f"{prefix}{pkg_dir.name}/{layer_info}")
spec_tree = "\n".join(tree_lines)
project_structure = f"""## Project Spec Directory Structure
```
{spec_tree}
```
To get structured package info, run: `python ./{DIR_WORKFLOW}/scripts/get_context.py --mode packages`
## Search Tips
- Spec files: `{spec_path}/**/*.md`
- Code search: Use Glob and Grep tools
- Tech solutions: Use mcp__exa__web_search_exa or mcp__exa__get_code_context_exa"""
context_parts.append(project_structure)
return "\n\n".join(context_parts)
def build_research_prompt(original_prompt: str, context: str) -> str:
"""Build complete prompt for Research"""
return f"""# Research Agent Task
You are the Research Agent in the Multi-Agent Pipeline (search researcher).
## Core Principle
**You do one thing: find and explain information.**
You are a documenter, not a reviewer.
## Project Info
{context}
---
## Your Task
{original_prompt}
---
## Workflow
1. **Understand query** - Determine search type (internal/external) and scope
2. **Plan search** - List search steps for complex queries
3. **Execute search** - Execute multiple independent searches in parallel
4. **Organize results** - Output structured report
## Search Tools
| Tool | Purpose |
|------|---------|
| Glob | Search by filename pattern |
| Grep | Search by content |
| Read | Read file content |
| mcp__exa__web_search_exa | External web search |
| mcp__exa__get_code_context_exa | External code/doc search |
## Strict Boundaries
**Only allowed**: Describe what exists, where it is, how it works
**Forbidden** (unless explicitly asked):
- Suggest improvements
- Criticize implementation
- Recommend refactoring
- Modify any files
## Report Format
Provide structured search results including:
- List of files found (with paths)
- Code pattern analysis (if applicable)
- Related spec documents
- External references (if any)"""
def _string_value(value: Any) -> str:
if isinstance(value, str):
stripped = value.strip()
return stripped
return ""
def _extract_subagent_name(value: Any) -> str:
"""Extract a sub-agent name from common platform encodings.
Cursor's native Task args encode custom sub-agents as a protobuf oneof,
which can appear in hook JSON as either ``{"custom": {"name": "..."}}``
or ``{"type": {"case": "custom", "value": {"name": "..."}}}``.
"""
direct = _string_value(value)
if direct:
return direct
if not isinstance(value, dict):
return ""
for key in ("name", "subagent_type_name", "subagentTypeName"):
direct = _string_value(value.get(key))
if direct:
return direct
custom = value.get("custom")
if isinstance(custom, dict):
custom_name = _string_value(custom.get("name"))
if custom_name:
return custom_name
oneof = value.get("type")
if isinstance(oneof, dict):
case_name = _string_value(oneof.get("case"))
if case_name == "custom":
nested_value = oneof.get("value")
if isinstance(nested_value, dict):
custom_name = _string_value(nested_value.get("name"))
if custom_name:
return custom_name
if case_name:
return case_name
case_name = _string_value(value.get("case"))
if case_name == "custom":
nested_value = value.get("value")
if isinstance(nested_value, dict):
custom_name = _string_value(nested_value.get("name"))
if custom_name:
return custom_name
if case_name:
return case_name
for agent_name in AGENTS_ALL:
if agent_name in value:
return agent_name
return ""
def _extract_subagent_type(tool_input: dict) -> str:
for key in (
"subagent_type",
"subagentType",
"subagent_type_name",
"subagentTypeName",
"agent_type",
"agentType",
"name",
):
agent_name = _extract_subagent_name(tool_input.get(key))
if agent_name:
return agent_name
return ""
def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]:
"""Parse hook input across different platform formats.
Returns (subagent_type, original_prompt, tool_input).
Handles:
- Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type
- Cursor: tool_name=Task|Subagent, tool_input.subagent_type
- Copilot CLI: toolName=task (camelCase key, lowercase value)
- Gemini CLI: tool_name IS the agent name (BeforeTool matcher already filtered)
- Kiro: agentSpawn hook, agent_name field at top level
"""
tool_input = input_data.get("tool_input", {})
# Standard format: Task/Agent tool with subagent_type
tool_name = input_data.get("tool_name", "") or input_data.get("toolName", "")
if tool_name.lower() in ("task", "agent", "subagent"):
return (
_extract_subagent_type(tool_input),
tool_input.get("prompt", ""),
tool_input,
)
# Kiro: agentSpawn hook passes agent_name at top level
agent_name = input_data.get("agent_name", "")
if agent_name:
return agent_name, tool_input.get("prompt", input_data.get("prompt", "")), tool_input
# Gemini CLI: BeforeTool where tool_name IS the agent name
# (matcher already ensured it's one of our agents)
if tool_name in AGENTS_ALL:
return tool_name, tool_input.get("prompt", ""), tool_input
# Copilot CLI: toolName field (camelCase), value might be the agent name
tool_name_camel = input_data.get("toolName", "")
if tool_name_camel in AGENTS_ALL:
return tool_name_camel, input_data.get("toolArgs", ""), tool_input
return "", "", tool_input
def main():
if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
sys.exit(0)
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError:
sys.exit(0)
subagent_type, original_prompt, tool_input = _parse_hook_input(input_data)
cwd = input_data.get("cwd", os.getcwd())
# Only handle subagent types we care about
if subagent_type not in AGENTS_ALL:
sys.exit(0)
# Find repo root
repo_root = find_repo_root(cwd)
if not repo_root:
sys.exit(0)
# Get current task directory (research doesn't require it)
task_dir = get_current_task(repo_root, input_data)
# implement/check need task directory
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)
if not os.path.exists(task_dir_full):
sys.exit(0)
# Check for [finish] marker in prompt (check agent with finish context)
is_finish_phase = "[finish]" in original_prompt.lower()
# Get context and build prompt based on subagent type
if subagent_type == AGENT_IMPLEMENT:
assert task_dir is not None # validated above
context = get_implement_context(repo_root, task_dir)
new_prompt = build_implement_prompt(original_prompt, context)
elif subagent_type == AGENT_CHECK:
assert task_dir is not None # validated above
if is_finish_phase:
# Finish phase: use finish context (lighter, focused on final verification)
context = get_finish_context(repo_root, task_dir)
new_prompt = build_finish_prompt(original_prompt, context)
else:
# Regular check phase: use check context (full specs for self-fix loop)
context = get_check_context(repo_root, task_dir)
new_prompt = build_check_prompt(original_prompt, context)
elif subagent_type == AGENT_RESEARCH:
# Research can work without task directory
context = get_research_context(repo_root, task_dir)
new_prompt = build_research_prompt(original_prompt, context)
else:
sys.exit(0)
if not context:
sys.exit(0)
# Return updated input — use a multi-format output that covers all platforms.
# Most platforms ignore unrecognized fields, so we include multiple formats.
# The platform picks whichever fields it understands.
updated = {**tool_input, "prompt": new_prompt}
output = {
# Claude Code / Qoder / CodeBuddy / Droid format
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": updated,
},
# Cursor format
"permission": "allow",
"updated_input": updated,
# Gemini format
"updatedInput": updated,
}
print(json.dumps(output, ensure_ascii=False))
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""Trellis per-turn breadcrumb hook (UserPromptSubmit / BeforeAgent equivalent).
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.
The emitted ``hookEventName`` field is platform-aware: most hosts expect
``UserPromptSubmit`` (Claude Code naming, also accepted by Cursor / Qoder /
CodeBuddy / Droid / Codex / Copilot wiring), but Gemini CLI 0.40.x renamed
its per-turn event to ``BeforeAgent`` and its schema validator rejects the
legacy name. ``_detect_platform`` picks the right value at runtime.
Breadcrumb text is pulled exclusively from workflow.md
[workflow-state:STATUS] tag blocks — workflow.md is the single source of
truth. There are no fallback dicts in this script: when workflow.md is
missing or a tag is absent, the breadcrumb degrades to a generic
"Refer to workflow.md for current step." line so users see (and fix)
the broken state instead of the hook silently masking it.
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
"""
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,
)
def load_breadcrumbs(root: Path) -> dict[str, str]:
"""Parse workflow.md for [workflow-state:STATUS] blocks.
Returns {status: body_text}. workflow.md is the single source of
truth — there are no fallback dicts in this script. Missing tags
(or a missing/unreadable workflow.md) fall back to a generic line
in build_breadcrumb so users see the broken state and fix
workflow.md, rather than the hook silently masking the issue.
"""
workflow = root / ".trellis" / "workflow.md"
if not workflow.is_file():
return {}
try:
content = workflow.read_text(encoding="utf-8")
except OSError:
return {}
result: dict[str, str] = {}
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 (tag present in workflow.md) → detailed template body
- Unknown status (no tag, or workflow.md missing) → generic
"Refer to workflow.md for current step." line
- `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:
if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
return 0
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)
# Gemini CLI 0.40.x rejects "UserPromptSubmit" — its per-turn event is
# named "BeforeAgent". Other platforms (Claude/Cursor/Qoder/CodeBuddy/
# Droid/Codex/Copilot) accept the original Claude-style name.
hook_event_name = (
"BeforeAgent" if _detect_platform(data) == "gemini" else "UserPromptSubmit"
)
output = {
"hookSpecificOutput": {
"hookEventName": hook_event_name,
"additionalContext": breadcrumb,
}
}
print(json.dumps(output))
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,780 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Session Start Hook - Inject structured context
"""
from __future__ import annotations
# IMPORTANT: Suppress all warnings FIRST
import warnings
warnings.filterwarnings("ignore")
import json
import os
import re
import shlex
import subprocess
import sys
from io import StringIO
from pathlib import Path
def _normalize_windows_shell_path(path_str: str) -> str:
"""Normalize Unix-style shell paths to real Windows paths.
On Windows, shells like Git Bash / MSYS2 / Cygwin may report paths like
`/d/Users/...` or `/cygdrive/d/Users/...`. `Path.resolve()` will misinterpret
these as `D:/d/Users...` on drive D: (or similar), breaking repo root
detection.
This function is intentionally conservative: it only rewrites patterns that
unambiguously represent a drive letter mount.
"""
if not isinstance(path_str, str) or not path_str:
return path_str
# Only relevant on Windows; keep other platforms untouched.
if not sys.platform.startswith("win"):
return path_str
p = path_str.strip()
# Already a Windows drive path (C:\... or C:/...)
if re.match(r"^[A-Za-z]:[\/]", p):
return p
# MSYS/Git-Bash style: /c/Users/... or /d/Work/...
m = re.match(r"^/([A-Za-z])/(.*)", p)
if m:
drive, rest = m.group(1).upper(), m.group(2)
rest = rest.replace('/', '\\')
return f"{drive}:\\{rest}"
# Cygwin style: /cygdrive/c/Users/...
m = re.match(r"^/cygdrive/([A-Za-z])/(.*)", p)
if m:
drive, rest = m.group(1).upper(), m.group(2)
rest = rest.replace('/', '\\')
return f"{drive}:\\{rest}"
# WSL mounted drive (sometimes leaked into env): /mnt/c/Users/...
m = re.match(r"^/mnt/([A-Za-z])/(.*)", p)
if m:
drive, rest = m.group(1).upper(), m.group(2)
rest = rest.replace('/', '\\')
return f"{drive}:\\{rest}"
return path_str
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>"""
# IMPORTANT: Force stdout to use UTF-8 on Windows
# This fixes UnicodeEncodeError when outputting non-ASCII characters
if sys.platform.startswith("win"):
import io as _io
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
elif hasattr(sys.stdout, "detach"):
sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr]
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 hook-inject and pull-based
sub-agent context loaders.
"""
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 should_skip_injection() -> bool:
"""Check if any platform's non-interactive flag is set, or if Trellis
hooks are explicitly disabled via TRELLIS_HOOKS=0 / TRELLIS_DISABLE_HOOKS=1.
"""
if os.environ.get("TRELLIS_HOOKS") == "0":
return True
if os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
return True
non_interactive_vars = [
"CLAUDE_NON_INTERACTIVE",
"QODER_NON_INTERACTIVE",
"CODEBUDDY_NON_INTERACTIVE",
"FACTORY_NON_INTERACTIVE",
"CURSOR_NON_INTERACTIVE",
"GEMINI_NON_INTERACTIVE",
"KIRO_NON_INTERACTIVE",
"COPILOT_NON_INTERACTIVE",
]
return any(os.environ.get(var) == "1" for var in non_interactive_vars)
def read_file(path: Path, fallback: str = "") -> str:
try:
return path.read_text(encoding="utf-8")
except (FileNotFoundError, PermissionError):
return fallback
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_context_key(trellis_dir: Path, input_data: dict) -> str | None:
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_context_key # type: ignore[import-not-found]
return resolve_context_key(input_data, platform=_detect_platform(input_data))
def _persist_context_key_for_bash(context_key: str | None) -> None:
"""Expose Trellis session identity to later Claude Code Bash commands.
Claude Code SessionStart hooks can append exports to CLAUDE_ENV_FILE; those
variables are then available to Bash tools in the same conversation. Without
this bridge, `task.py start` has hook stdin during SessionStart but no
session identity when the AI later runs it as a normal shell command.
"""
if not context_key:
return
env_file = os.environ.get("CLAUDE_ENV_FILE")
if not env_file:
return
try:
with open(env_file, "a", encoding="utf-8") as handle:
handle.write(f"export TRELLIS_CONTEXT_ID={shlex.quote(context_key)}\n")
except OSError:
pass
def _resolve_active_task(trellis_dir: Path, input_data: 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,
input_data,
platform=_detect_platform(input_data),
)
def run_script(script_path: Path, context_key: str | None = None) -> str:
try:
if script_path.suffix == ".py":
# Add PYTHONIOENCODING to force UTF-8 in subprocess
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)]
else:
env = os.environ.copy()
if context_key:
env["TRELLIS_CONTEXT_ID"] = context_key
cmd = [str(script_path)]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
cwd=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, input_data: dict) -> str:
"""Check current task status and return structured status string with explicit next action.
Returns a block with three fields:
- Status: current state
- Task: task identifier (when applicable)
- Next-Action: explicit skill/command/tool call the AI should invoke
"""
active = _resolve_active_task(trellis_dir, input_data)
# Case 1: No active task — waiting for user to describe intent
if not active.task_path:
return (
"Status: NO ACTIVE TASK\n"
f"Source: {active.source}\n"
"Next-Action: After the user describes their intent, load skill `trellis-brainstorm` "
"to clarify requirements and create a task via `python ./.trellis/scripts/task.py create`.\n"
"Research reminder: for research-heavy tasks (comparing tools, reading external docs, "
"cross-platform surveys), spawn `trellis-research` sub-agents via the Task tool — "
"they persist findings to `{TASK_DIR}/research/*.md` and keep main context clean. "
"Do NOT do 10+ inline WebFetch/WebSearch in the main conversation.\n"
"User override (per-turn escape hatch): if the user's first message explicitly opts "
"out of the workflow (\"跳过 trellis\" / \"别走流程\" / \"小修一下\" / \"直接改\" / "
"\"skip trellis\" / \"no task\" / \"just do it\"), honor it for this turn — "
"acknowledge briefly and proceed without creating a task. Per-turn only."
)
# Case 2: Stale pointer — task dir was deleted
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}\n"
f"Source: {active.source}\n"
f"Next-Action: Run `python ./.trellis/scripts/task.py finish` to clear the stale pointer, "
"then ask the user what to work on next."
)
# Read task.json
task_json_path = task_dir / "task.json"
task_data = {}
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")
# Case 3: Task completed — time to archive
if task_status == "completed":
return (
f"Status: COMPLETED\nTask: {task_title}\n"
f"Source: {active.source}\n"
f"Next-Action: Load skill `trellis-update-spec` to capture learnings, "
f"then archive with `python ./.trellis/scripts/task.py archive {task_dir.name}`."
)
has_prd = (task_dir / "prd.md").is_file()
# Case 4: No PRD — still in Plan phase
if not has_prd:
return (
f"Status: PLANNING\nTask: {task_title}\n"
f"Source: {active.source}\n"
"Next-Action: Load skill `trellis-brainstorm` to clarify requirements with the user "
"and produce prd.md in the task directory.\n"
"Research reminder: when the task needs external research (tool comparison, docs, "
"conventions survey), spawn `trellis-research` sub-agents — don't WebFetch/WebSearch "
"inline in the main session. Findings go to `{task_dir}/research/*.md`; PRD only links to them."
)
# Case 4b: PRD exists but implement.jsonl has only seed (no curated entries) — Phase 1.3 gate
implement_jsonl = task_dir / "implement.jsonl"
if implement_jsonl.is_file() and not _has_curated_jsonl_entry(implement_jsonl):
return (
f"Status: PLANNING (Phase 1.3)\nTask: {task_title}\n"
f"Source: {active.source}\n"
"Next-Action: Curate `implement.jsonl` and `check.jsonl` with the spec + research files "
"the Phase 2 sub-agents will need. Only spec paths (`.trellis/spec/**/*.md`) and research "
"files (`{TASK_DIR}/research/*.md`) — no code paths. Run "
"`python ./.trellis/scripts/get_context.py --mode packages` to list available specs, "
"then edit the jsonl files or use `python ./.trellis/scripts/task.py add-context`. "
"See `.trellis/workflow.md` Phase 1.3 for details."
)
# Case 5: PRD + curated jsonl (or agent-less platform with no jsonl) — enter Execute phase
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"
"Sub-agent roster: `trellis-implement` (writes code), `trellis-check` (verifies + self-fixes), "
"`trellis-research` (persists findings to `research/*.md` — use when you'd otherwise do "
"multiple WebFetch/WebSearch inline).\n"
"Sub-agent self-exemption: if you are reading this as a `trellis-implement` or "
"`trellis-check` sub-agent (your own role / agent name reflects that), this dispatch "
"instruction does NOT apply to you — you are already the dispatched sub-agent. "
"Implement / check directly without spawning another sub-agent of the same kind.\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 _load_trellis_config(trellis_dir: Path, input_data: dict) -> tuple:
"""Load Trellis config for session-start decisions.
Returns:
(is_mono, packages_dict, spec_scope, task_pkg, default_pkg)
"""
scripts_dir = trellis_dir / "scripts"
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
try:
from common.config import get_default_package, get_packages, get_spec_scope, is_monorepo # type: ignore[import-not-found]
from common.paths import get_current_task # type: ignore[import-not-found]
repo_root = trellis_dir.parent
is_mono = is_monorepo(repo_root)
packages = get_packages(repo_root) or {}
scope = get_spec_scope(repo_root)
# Get active task's package
task_pkg = None
current = get_current_task(
repo_root,
input_data,
platform=_detect_platform(input_data),
)
if current:
task_json = repo_root / current / "task.json"
if task_json.is_file():
try:
data = json.loads(task_json.read_text(encoding="utf-8"))
if isinstance(data, dict):
tp = data.get("package")
if isinstance(tp, str) and tp:
task_pkg = tp
except (json.JSONDecodeError, OSError):
pass
default_pkg = get_default_package(repo_root)
return is_mono, packages, scope, task_pkg, default_pkg
except Exception:
return False, {}, None, None, None
def _check_legacy_spec(trellis_dir: Path, is_mono: bool, packages: dict) -> str | None:
"""Check for legacy spec directory structure in monorepo.
Returns warning message if legacy structure detected, None otherwise.
"""
if not is_mono or not packages:
return None
spec_dir = trellis_dir / "spec"
if not spec_dir.is_dir():
return None
# Check for legacy flat spec dirs (spec/backend/, spec/frontend/ with index.md)
has_legacy = False
for legacy_name in ("backend", "frontend"):
legacy_dir = spec_dir / legacy_name
if legacy_dir.is_dir() and (legacy_dir / "index.md").is_file():
has_legacy = True
break
if not has_legacy:
return None
# Check which packages are missing spec/<pkg>/ directory
missing = [
name for name in sorted(packages.keys())
if not (spec_dir / name).is_dir()
]
if not missing:
return None # All packages have spec dirs
if len(missing) == len(packages):
return (
f"[!] Legacy spec structure detected: found `spec/backend/` or `spec/frontend/` "
f"but no package-scoped `spec/<package>/` directories.\n"
f"Monorepo packages: {', '.join(sorted(packages.keys()))}\n"
f"Please reorganize: `spec/backend/` -> `spec/<package>/backend/`"
)
return (
f"[!] Partial spec migration detected: packages {', '.join(missing)} "
f"still missing `spec/<pkg>/` directory.\n"
f"Please complete migration for all packages."
)
def _resolve_spec_scope(
is_mono: bool,
packages: dict,
scope,
task_pkg: str | None,
default_pkg: str | None,
) -> set | None:
"""Resolve which packages should have their specs injected.
Returns:
Set of package names to include, or None for full scan.
"""
if not is_mono or not packages:
return None # Single-repo: full scan
if scope is None:
return None # No scope configured: full scan
if isinstance(scope, str) and scope == "active_task":
if task_pkg and task_pkg in packages:
return {task_pkg}
if default_pkg and default_pkg in packages:
return {default_pkg}
return None # Fallback to full scan
if isinstance(scope, list):
valid = set()
for entry in scope:
if entry in packages:
valid.add(entry)
else:
print(
f"Warning: spec_scope contains unknown package: {entry}, ignoring",
file=sys.stderr,
)
if valid:
# Warn if active task is out of scope
if task_pkg and task_pkg not in valid:
print(
f"Warning: active task package '{task_pkg}' is out of configured spec_scope",
file=sys.stderr,
)
return valid
# All entries invalid: fallback chain
print(
"Warning: all spec_scope entries invalid, falling back to task/default/full",
file=sys.stderr,
)
if task_pkg and task_pkg in packages:
return {task_pkg}
if default_pkg and default_pkg in packages:
return {default_pkg}
return None # Full scan
return None # Unknown scope type: full scan
def _extract_range(content: str, start_header: str, end_header: str) -> str:
"""Extract lines starting at `## start_header` up to (but excluding) `## end_header`.
Both parameters are full header lines WITHOUT the `## ` prefix (e.g. "Phase Index").
Returns empty string if start header is not found.
End header missing → extracts to end of file.
"""
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()
_BREADCRUMB_TAG_RE = re.compile(
r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]",
re.DOTALL,
)
def _strip_breadcrumb_tag_blocks(content: str) -> str:
"""Remove `[workflow-state:STATUS]...[/workflow-state:STATUS]` blocks.
The tag blocks live inside `## Phase Index` (since v0.5.0-rc.0, when
they were colocated with their phase summaries) and are consumed by the
UserPromptSubmit hook (`inject-workflow-state.py`). The session-start
payload already covers the full step bodies, so re-inlining the
breadcrumbs here would just duplicate context.
"""
return _BREADCRUMB_TAG_RE.sub("", content)
def _build_workflow_overview(workflow_path: Path) -> str:
"""Inject the workflow guide for the session.
Contents:
1. Section index (all `## ` headings — navigation)
2. Phase Index section (rules, skill routing table, anti-rationalization table)
3. Phase 1/2/3 step-level details (the actual how-to for each step)
The meta sections (Core Principles / Trellis System / Customizing
Trellis) are NOT injected — Core Principles is short prose the AI can
Read on demand; Trellis System lists reference commands duplicated in
step bodies; Customizing Trellis is for forks. Workflow-state breadcrumb
tag blocks (which now live inside Phase Index since v0.5.0-rc.0) are
stripped from the extracted range — they're consumed by the
UserPromptSubmit hook, not the session-start preamble.
Total budget: Phase Index ~2 KB + Phase 1/2/3 ~7 KB = ~9 KB.
"""
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 += ["", "---", ""]
# Extract Phase Index through the end of Phase 3 (before "Customizing
# Trellis" — the docs-for-forks footer added in v0.5.0-rc.0). Since
# sections appear in order Phase Index → Phase 1 → Phase 2 → Phase 3 →
# Customizing Trellis, a single range grab captures all four. The
# breadcrumb tag blocks now embedded inside Phase Index are stripped so
# they don't duplicate the per-turn UserPromptSubmit injection.
phases = _extract_range(
content, "Phase Index", "Customizing Trellis (for forks)"
)
if phases:
out_lines.append(_strip_breadcrumb_tag_blocks(phases).rstrip())
return "\n".join(out_lines).rstrip()
def main():
if should_skip_injection():
sys.exit(0)
try:
hook_input = json.loads(sys.stdin.read())
if not isinstance(hook_input, dict):
hook_input = {}
except (json.JSONDecodeError, ValueError):
hook_input = {}
# Try platform-specific env vars, hook cwd, fallback to cwd
project_dir_env_vars = [
"CLAUDE_PROJECT_DIR",
"QODER_PROJECT_DIR",
"CODEBUDDY_PROJECT_DIR",
"FACTORY_PROJECT_DIR",
"CURSOR_PROJECT_DIR",
"GEMINI_PROJECT_DIR",
"KIRO_PROJECT_DIR",
"COPILOT_PROJECT_DIR",
]
project_dir = None
for var in project_dir_env_vars:
val = os.environ.get(var)
if val:
project_dir = Path(_normalize_windows_shell_path(val)).resolve()
break
if project_dir is None:
project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve()
trellis_dir = project_dir / ".trellis"
context_key = _resolve_context_key(trellis_dir, hook_input)
_persist_context_key_for_bash(context_key)
# Load config for scope filtering and legacy detection
is_mono, packages, scope_config, task_pkg, default_pkg = _load_trellis_config(
trellis_dir,
hook_input,
)
allowed_pkgs = _resolve_spec_scope(is_mono, packages, scope_config, task_pkg, default_pkg)
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")
# Legacy migration warning
legacy_warning = _check_legacy_spec(trellis_dir, is_mono, packages)
if legacy_warning:
output.write(f"<migration-warning>\n{legacy_warning}\n</migration-warning>\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_overview(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 "
"or loaded by the sub-agent 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"
"- Sub-agent self-exemption: if you are reading this as a `trellis-implement` "
"or `trellis-check` sub-agent, the \"dispatch trellis-implement / trellis-check\" "
"rule above does NOT apply to you — you are already the dispatched sub-agent. "
"Do NOT spawn another sub-agent of the same kind; implement / check directly.\n\n"
)
# guides/ is cross-package thinking — always include inline (small, 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 spec indexes — paths only (main agent reads on demand;
# sub-agents get their specific specs via jsonl injection)
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 # already inlined above
index_file = sub / "index.md"
if index_file.is_file():
# Flat spec dir (single-repo layer like spec/backend/)
paths.append(f".trellis/spec/{sub.name}/index.md")
else:
# Nested package dirs (monorepo: spec/<pkg>/<layer>/index.md)
# Apply scope filter
if allowed_pkgs is not None and sub.name not in allowed_pkgs:
continue
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: "
"`python ./.trellis/scripts/get_context.py --mode packages`\n"
)
output.write("</guidelines>\n\n")
# Check task status and inject structured tag
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>""")
result = {
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": output.getvalue(),
}
}
# Output JSON - stdout is already configured for UTF-8
print(json.dumps(result, ensure_ascii=False), flush=True)
if __name__ == "__main__":
main()

73
.claude/settings.json Normal file
View File

@@ -0,0 +1,73 @@
{
"env": {
"CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": "1"
},
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/session-start.py",
"timeout": 10
}
]
},
{
"matcher": "clear",
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/session-start.py",
"timeout": 10
}
]
},
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/session-start.py",
"timeout": 10
}
]
}
],
"PreToolUse": [
{
"matcher": "Task",
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/inject-subagent-context.py",
"timeout": 30
}
]
},
{
"matcher": "Agent",
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/inject-subagent-context.py",
"timeout": 30
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/inject-workflow-state.py",
"timeout": 5
}
]
}
]
},
"enabledPlugins": {}
}

View File

@@ -0,0 +1,34 @@
---
name: trellis-before-dev
description: "Discovers and injects project-specific coding guidelines from .trellis/spec/ before implementation begins. Reads spec indexes, pre-development checklists, and shared thinking guides for the target package. Use when starting a new coding task, before writing any code, switching to a different package, or needing to refresh project conventions and standards."
---
Read the relevant development guidelines before starting your task.
Execute these steps:
1. **Discover packages and their spec layers**:
```bash
python ./.trellis/scripts/get_context.py --mode packages
```
2. **Identify which specs apply** to your task based on:
- Which package you're modifying (e.g., `cli/`, `docs-site/`)
- What type of work (backend, frontend, unit-test, docs, etc.)
3. **Read the spec index** for each relevant module:
```bash
cat .trellis/spec/<package>/<layer>/index.md
```
Follow the **"Pre-Development Checklist"** section in the index.
4. **Read the specific guideline files** listed in the Pre-Development Checklist that are relevant to your task. The index is NOT the goal — it points you to the actual guideline files (e.g., `error-handling.md`, `conventions.md`, `mock-strategies.md`). Read those files to understand the coding standards and patterns.
5. **Always read shared guides**:
```bash
cat .trellis/spec/guides/index.md
```
6. Understand the coding standards and patterns you need to follow, then proceed with your development plan.
This step is **mandatory** before writing any code.

View File

@@ -0,0 +1,538 @@
---
name: trellis-brainstorm
description: "Guides collaborative requirements discovery before implementation. Creates task directory, seeds PRD, asks high-value questions one at a time, researches technical choices, and converges on MVP scope. Use when requirements are unclear, there are multiple valid approaches, or the user describes a new feature or complex task."
---
# Brainstorm - Requirements Discovery (AI Coding Enhanced)
Guide AI through collaborative requirements discovery **before implementation**, optimized for AI coding workflows:
* **Task-first** (capture ideas immediately)
* **Action-before-asking** (reduce low-value questions)
* **Research-first** for technical choices (avoid asking users to invent options)
* **Diverge → Converge** (expand thinking, then lock MVP)
---
## When to Use
Triggered from /trellis:start when the user describes a development task, especially when:
* requirements are unclear or evolving
* there are multiple valid implementation paths
* trade-offs matter (UX, reliability, maintainability, cost, performance)
* the user might not know the best options up front
---
## Core Principles (Non-negotiable)
1. **Task-first (capture early)**
Always ensure a task exists at the start so the user's ideas are recorded immediately.
2. **Action before asking**
If you can derive the answer from repo code, docs, configs, conventions, or quick research — do that first.
3. **One question per message**
Never overwhelm the user with a list of questions. Ask one, update PRD, repeat.
4. **Prefer concrete options**
For preference/decision questions, present 23 feasible, specific approaches with trade-offs.
5. **Research-first for technical choices**
If the decision depends on industry conventions / similar tools / established patterns, do research first, then propose options.
6. **Diverge → Converge**
After initial understanding, proactively consider future evolution, related scenarios, and failure/edge cases — then converge to an MVP with explicit out-of-scope.
7. **No meta questions**
Do not ask "should I search?" or "can you paste the code so I can continue?"
If you need information: search/inspect. If blocked: ask the minimal blocking question.
---
## Step 0: Ensure Task Exists (ALWAYS)
Before any Q&A, ensure a task exists. If none exists, create one immediately.
* Use a **temporary working title** derived from the user's message.
* It's OK if the title is imperfect — refine later in PRD.
```bash
TASK_DIR=$(python ./.trellis/scripts/task.py create "brainstorm: <short goal>" --slug <auto>)
```
Use a slug without a date prefix. `task.py create` adds the `MM-DD-`
directory prefix automatically.
Create/seed `prd.md` immediately with what you know:
```markdown
# brainstorm: <short goal>
## Goal
<one paragraph: what + why>
## What I already know
* <facts from user message>
* <facts discovered from repo/docs>
## Assumptions (temporary)
* <assumptions to validate>
## Open Questions
* <ONLY Blocking / Preference questions; keep list short>
## Requirements (evolving)
* <start with what is known>
## Acceptance Criteria (evolving)
* [ ] <testable criterion>
## Definition of Done (team quality bar)
* Tests added/updated (unit/integration where appropriate)
* Lint / typecheck / CI green
* Docs/notes updated if behavior changes
* Rollout/rollback considered if risky
## Out of Scope (explicit)
* <what we will not do in this task>
## Technical Notes
* <files inspected, constraints, links, references>
* <research notes summary if applicable>
```
---
## Step 1: Auto-Context (DO THIS BEFORE ASKING QUESTIONS)
Before asking questions like "what does the code look like?", gather context yourself:
### Repo inspection checklist
* Identify likely modules/files impacted
* Locate existing patterns (similar features, conventions, error handling style)
* Check configs, scripts, existing command definitions
* Note any constraints (runtime, dependency policy, build tooling)
### Documentation checklist
* Look for existing PRDs/specs/templates
* Look for command usage examples, README, ADRs if any
Write findings into PRD:
* Add to `What I already know`
* Add constraints/links to `Technical Notes`
---
## Step 2: Classify Complexity (still useful, not gating task creation)
| Complexity | Criteria | Action |
| ------------ | ------------------------------------------------------ | ------------------------------------------- |
| **Trivial** | Single-line fix, typo, obvious change | Skip brainstorm, implement directly |
| **Simple** | Clear goal, 12 files, scope well-defined | Ask 1 confirm question, then implement |
| **Moderate** | Multiple files, some ambiguity | Light brainstorm (23 high-value questions) |
| **Complex** | Vague goal, architectural choices, multiple approaches | Full brainstorm |
> Note: Task already exists from Step 0. Classification only affects depth of brainstorming.
---
## Step 3: Question Gate (Ask ONLY high-value questions)
Before asking ANY question, run the following gate:
### Gate A — Can I derive this without the user?
If answer is available via:
* repo inspection (code/config)
* docs/specs/conventions
* quick market/OSS research
**Do not ask.** Fetch it, summarize, update PRD.
### Gate B — Is this a meta/lazy question?
Examples:
* "Should I search?"
* "Can you paste the code so I can proceed?"
* "What does the code look like?" (when repo is available)
**Do not ask.** Take action.
### Gate C — What type of question is it?
* **Blocking**: cannot proceed without user input
* **Preference**: multiple valid choices, depends on product/UX/risk preference
* **Derivable**: should be answered by inspection/research
→ Only ask **Blocking** or **Preference**.
---
## Step 4: Research-first Mode (Mandatory for technical choices)
### Trigger conditions (any → research-first)
* The task involves selecting an approach, library, protocol, framework, template system, plugin mechanism, or CLI UX convention
* The user asks for "best practice", "how others do it", "recommendation"
* The user can't reasonably enumerate options
### Delegate to `trellis-research` sub-agent (don't research inline)
For each research topic, **spawn a `trellis-research` sub-agent via the Task tool** — don't do WebFetch / WebSearch / `gh api` inline in the main conversation.
Why:
- The sub-agent has its own context window → doesn't pollute brainstorm context with raw tool output
- It persists findings to `{TASK_DIR}/research/<topic>.md` (the contract — see `workflow.md` Phase 1.2)
- It returns only `{file path, one-line summary}` to the main agent
- Independent topics can be **parallelized** — spawn multiple sub-agents in one tool call
Agent type: `trellis-research`
Task description template: "Research <specific question>; persist findings to `{TASK_DIR}/research/<topic-slug>.md`."
❌ Bad (what you must NOT do):
```
Main agent: WebFetch(url-A) → WebFetch(url-B) → Bash(gh api ...)
→ WebSearch(q1) → WebSearch(q2) → ... (10+ inline calls)
→ Write(research/topic.md)
```
→ Pollutes main context with raw HTML/JSON, burns tokens.
✅ Good:
```
Main agent: Task(subagent_type="trellis-research",
prompt="Research topic A; persist to research/topic-a.md")
+ Task(subagent_type="trellis-research",
prompt="Research topic B; persist to research/topic-b.md")
+ Task(subagent_type="trellis-research",
prompt="Research topic C; persist to research/topic-c.md")
→ Reads research/topic-{a,b,c}.md after they finish.
```
### Research steps (to pass into each sub-agent prompt)
Each `trellis-research` sub-agent should:
1. Identify 24 comparable tools/patterns for its topic
2. Summarize common conventions and why they exist
3. Map conventions onto our repo constraints
4. Write findings to `{TASK_DIR}/research/<topic>.md`
Main agent then reads the persisted files and produces **23 feasible approaches** in PRD.
### Research output format (PRD)
The PRD itself should only reference the persisted research files, not duplicate their content. Add a `## Research References` section pointing at `research/*.md`.
Optionally, add a convergence section with feasible approaches derived from the research:
```markdown
## Research References
* [`research/<topic-a>.md`](research/<topic-a>.md) — <one-line takeaway>
* [`research/<topic-b>.md`](research/<topic-b>.md) — <one-line takeaway>
## Research Notes
### What similar tools do
* ...
* ...
### Constraints from our repo/project
* ...
### Feasible approaches here
**Approach A: <name>** (Recommended)
* How it works:
* Pros:
* Cons:
**Approach B: <name>**
* How it works:
* Pros:
* Cons:
**Approach C: <name>** (optional)
* ...
```
Then ask **one** preference question:
* "Which approach do you prefer: A / B / C (or other)?"
---
## Step 5: Expansion Sweep (DIVERGE) — Required after initial understanding
After you can summarize the goal, proactively broaden thinking before converging.
### Expansion categories (keep to 12 bullets each)
1. **Future evolution**
* What might this feature become in 13 months?
* What extension points are worth preserving now?
2. **Related scenarios**
* What adjacent commands/flows should remain consistent with this?
* Are there parity expectations (create vs update, import vs export, etc.)?
3. **Failure & edge cases**
* Conflicts, offline/network failure, retries, idempotency, compatibility, rollback
* Input validation, security boundaries, permission checks
### Expansion message template (to user)
```markdown
I understand you want to implement: <current goal>.
Before diving into design, let me quickly diverge to consider three categories (to avoid rework later):
1. Future evolution: <12 bullets>
2. Related scenarios: <12 bullets>
3. Failure/edge cases: <12 bullets>
For this MVP, which would you like to include (or none)?
1. Current requirement only (minimal viable)
2. Add <X> (reserve for future extension)
3. Add <Y> (improve robustness/consistency)
4. Other: describe your preference
```
Then update PRD:
* What's in MVP → `Requirements`
* What's excluded → `Out of Scope`
---
## Step 6: Q&A Loop (CONVERGE)
### Rules
* One question per message
* Prefer multiple-choice when possible
* After each user answer:
* Update PRD immediately
* Move answered items from `Open Questions``Requirements`
* Update `Acceptance Criteria` with testable checkboxes
* Clarify `Out of Scope`
### Question priority (recommended)
1. **MVP scope boundary** (what is included/excluded)
2. **Preference decisions** (after presenting concrete options)
3. **Failure/edge behavior** (only for MVP-critical paths)
4. **Success metrics & Acceptance Criteria** (what proves it works)
### Preferred question format (multiple choice)
```markdown
For <topic>, which approach do you prefer?
1. **Option A** — <what it means + trade-off>
2. **Option B** — <what it means + trade-off>
3. **Option C** — <what it means + trade-off>
4. **Other** — describe your preference
```
---
## Step 7: Propose Approaches + Record Decisions (Complex tasks)
After requirements are clear enough, propose 23 approaches (if not already done via research-first):
```markdown
Based on current information, here are 23 feasible approaches:
**Approach A: <name>** (Recommended)
* How:
* Pros:
* Cons:
**Approach B: <name>**
* How:
* Pros:
* Cons:
Which direction do you prefer?
```
Record the outcome in PRD as an ADR-lite section:
```markdown
## Decision (ADR-lite)
**Context**: Why this decision was needed
**Decision**: Which approach was chosen
**Consequences**: Trade-offs, risks, potential future improvements
```
---
## Step 8: Final Confirmation + Implementation Plan
When open questions are resolved, confirm complete requirements with a structured summary:
### Final confirmation format
```markdown
Here's my understanding of the complete requirements:
**Goal**: <one sentence>
**Requirements**:
* ...
* ...
**Acceptance Criteria**:
* [ ] ...
* [ ] ...
**Definition of Done**:
* ...
**Out of Scope**:
* ...
**Technical Approach**:
<brief summary + key decisions>
**Implementation Plan (small PRs)**:
* PR1: <scaffolding + tests + minimal plumbing>
* PR2: <core behavior>
* PR3: <edge cases + docs + cleanup>
Does this look correct? If yes, I'll proceed with implementation.
```
### Subtask Decomposition (Complex Tasks)
For complex tasks with multiple independent work items, create subtasks:
```bash
# Create child tasks
CHILD1=$(python ./.trellis/scripts/task.py create "Child task 1" --slug child1 --parent "$TASK_DIR")
CHILD2=$(python ./.trellis/scripts/task.py create "Child task 2" --slug child2 --parent "$TASK_DIR")
# Or link existing tasks
python ./.trellis/scripts/task.py add-subtask "$TASK_DIR" "$CHILD_DIR"
```
---
## PRD Target Structure (final)
`prd.md` should converge to:
```markdown
# <Task Title>
## Goal
<why + what>
## Requirements
* ...
## Acceptance Criteria
* [ ] ...
## Definition of Done
* ...
## Technical Approach
<key design + decisions>
## Decision (ADR-lite)
Context / Decision / Consequences
## Out of Scope
* ...
## Technical Notes
<constraints, references, files, research notes>
```
---
## Anti-Patterns (Hard Avoid)
* Asking user for code/context that can be derived from repo
* Asking user to choose an approach before presenting concrete options
* Meta questions about whether to research
* Staying narrowly on the initial request without considering evolution/edges
* Letting brainstorming drift without updating PRD
---
## Integration with Start Workflow
After brainstorm completes (Step 8 confirmation approved), the flow continues to the Task Workflow's **Phase 2: Prepare for Implementation**:
```text
Brainstorm
Step 0: Create task directory + seed PRD
Step 17: Discover requirements, research, converge
Step 8: Final confirmation → user approves
Task Workflow Phase 2 (Prepare for Implementation)
Code-Spec Depth Check (if applicable)
→ Research codebase (based on confirmed PRD)
→ Configure code-spec context (jsonl files)
→ Activate task
Task Workflow Phase 3 (Execute)
Implement → Check → Complete
```
The task directory and PRD already exist from brainstorm, so Phase 1 of the Task Workflow is skipped entirely.
---
## Related Commands
| Command | When to Use |
|---------|-------------|
| `/trellis:start` | Entry point that triggers brainstorm |
| `/trellis:finish-work` | After implementation is complete |
| `/trellis:update-spec` | If new patterns emerge during work |

View File

@@ -0,0 +1,130 @@
---
name: trellis-break-loop
description: "Deep bug analysis to break the fix-forget-repeat cycle. Analyzes root cause category, why fixes failed, prevention mechanisms, and captures knowledge into specs. Use after fixing a bug to prevent the same class of bugs."
---
# Break the Loop - Deep Bug Analysis
When debug is complete, use this for deep analysis to break the "fix bug -> forget -> repeat" cycle.
---
## Analysis Framework
Analyze the bug you just fixed from these 5 dimensions:
### 1. Root Cause Category
Which category does this bug belong to?
| Category | Characteristics | Example |
|----------|-----------------|---------|
| **A. Missing Spec** | No documentation on how to do it | New feature without checklist |
| **B. Cross-Layer Contract** | Interface between layers unclear | API returns different format than expected |
| **C. Change Propagation Failure** | Changed one place, missed others | Changed function signature, missed call sites |
| **D. Test Coverage Gap** | Unit test passes, integration fails | Works alone, breaks when combined |
| **E. Implicit Assumption** | Code relies on undocumented assumption | Timestamp seconds vs milliseconds |
### 2. Why Fixes Failed (if applicable)
If you tried multiple fixes before succeeding, analyze each failure:
- **Surface Fix**: Fixed symptom, not root cause
- **Incomplete Scope**: Found root cause, didn't cover all cases
- **Tool Limitation**: grep missed it, type check wasn't strict
- **Mental Model**: Kept looking in same layer, didn't think cross-layer
### 3. Prevention Mechanisms
What mechanisms would prevent this from happening again?
| Type | Description | Example |
|------|-------------|---------|
| **Documentation** | Write it down so people know | Update thinking guide |
| **Architecture** | Make the error impossible structurally | Type-safe wrappers |
| **Compile-time** | Strict type checking, no escape hatches | Signature change causes compile error |
| **Runtime** | Monitoring, alerts, scans | Detect orphan entities |
| **Test Coverage** | E2E tests, integration tests | Verify full flow |
| **Code Review** | Checklist, PR template | "Did you check X?" |
### 4. Systematic Expansion
What broader problems does this bug reveal?
- **Similar Issues**: Where else might this problem exist?
- **Design Flaw**: Is there a fundamental architecture issue?
- **Process Flaw**: Is there a development process improvement?
- **Knowledge Gap**: Is the team missing some understanding?
### 5. Knowledge Capture
Solidify insights into the system:
- [ ] Update `.trellis/spec/guides/` thinking guides
- [ ] Update relevant `.trellis/spec/` docs
- [ ] Create issue record (if applicable)
- [ ] Create feature ticket for root fix
- [ ] Update check guidelines if needed
---
## Output Format
Please output analysis in this format:
```markdown
## Bug Analysis: [Short Description]
### 1. Root Cause Category
- **Category**: [A/B/C/D/E] - [Category Name]
- **Specific Cause**: [Detailed description]
### 2. Why Fixes Failed (if applicable)
1. [First attempt]: [Why it failed]
2. [Second attempt]: [Why it failed]
...
### 3. Prevention Mechanisms
| Priority | Mechanism | Specific Action | Status |
|----------|-----------|-----------------|--------|
| P0 | ... | ... | TODO/DONE |
### 4. Systematic Expansion
- **Similar Issues**: [List places with similar problems]
- **Design Improvement**: [Architecture-level suggestions]
- **Process Improvement**: [Development process suggestions]
### 5. Knowledge Capture
- [ ] [Documents to update / tickets to create]
```
---
## Core Philosophy
> **The value of debugging is not in fixing the bug, but in making this class of bugs never happen again.**
Three levels of insight:
1. **Tactical**: How to fix THIS bug
2. **Strategic**: How to prevent THIS CLASS of bugs
3. **Philosophical**: How to expand thinking patterns
30 minutes of analysis saves 30 hours of future debugging.
---
## After Analysis: Immediate Actions
**IMPORTANT**: After completing the analysis above, you MUST immediately:
1. **Update spec/guides** - Don't just list TODOs, actually update the relevant files:
- If it's a cross-platform issue → update `cross-platform-thinking-guide.md`
- If it's a cross-layer issue → update `cross-layer-thinking-guide.md`
- If it's a code reuse issue → update `code-reuse-thinking-guide.md`
- If it's domain-specific → update `backend/*.md` or `frontend/*.md`
2. **Sync templates** - After updating `.trellis/spec/`, sync to `src/templates/markdown/spec/`
3. **Commit the spec updates** - This is the primary output, not just the analysis text
> **The analysis is worthless if it stays in chat. The value is in the updated specs.**

View File

@@ -0,0 +1,92 @@
---
name: trellis-check
description: "Comprehensive quality verification: spec compliance, lint, type-check, tests, cross-layer data flow, code reuse, and consistency checks. Use when code is written and needs quality verification, before committing changes, or to catch context drift during long sessions."
---
# Code Quality Check
Comprehensive quality verification for recently written code. Combines spec compliance, cross-layer safety, and pre-commit checks.
---
## Step 1: Identify What Changed
```bash
git diff --name-only HEAD
git status
```
## Step 2: Read Applicable Specs
```bash
python ./.trellis/scripts/get_context.py --mode packages
```
For each changed package/layer, read the spec index and follow its **Quality Check** section:
```bash
cat .trellis/spec/<package>/<layer>/index.md
```
Read the specific guideline files referenced — the index is a pointer, not the goal.
## Step 3: Run Project Checks
Run the project's lint, type-check, and test commands. Fix any failures before proceeding.
## Step 4: Review Against Checklist
### Code Quality
- [ ] Linter passes?
- [ ] Type checker passes (if applicable)?
- [ ] Tests pass?
- [ ] No debug logging left in?
- [ ] No suppressed warnings or type-safety bypasses?
### Test Coverage
- [ ] New function → unit test added?
- [ ] Bug fix → regression test added?
- [ ] Changed behavior → existing tests updated?
### Spec Sync
- [ ] Does `.trellis/spec/` need updates? (new patterns, conventions, lessons learned)
> "If I fixed a bug or discovered something non-obvious, should I document it so future me won't hit the same issue?" → If YES, update the relevant spec doc.
## Step 5: Cross-Layer Dimensions (if applicable)
Skip this step if your change is confined to a single layer.
### A. Data Flow (changes touch 3+ layers)
- [ ] Read flow traces correctly: Storage → Service → API → UI
- [ ] Write flow traces correctly: UI → API → Service → Storage
- [ ] Types/schemas correctly passed between layers?
- [ ] Errors properly propagated to caller?
### B. Code Reuse (modifying constants, creating utilities)
- [ ] Searched for existing similar code before creating new?
```bash
grep -r "pattern" src/
```
- [ ] If 2+ places define same value → extracted to shared constant?
- [ ] After batch modification, all occurrences updated?
### C. Import/Dependency (creating new files)
- [ ] Correct import paths (relative vs absolute)?
- [ ] No circular dependencies?
### D. Same-Layer Consistency
- [ ] Other places using the same concept are consistent?
---
## Step 6: Report and Fix
Report violations found and fix them directly. Re-run project checks after fixes.

View File

@@ -0,0 +1,73 @@
---
name: trellis-meta
description: "Understand and customize the local Trellis architecture inside a user project. Use when modifying .trellis plus platform hooks, settings, agents, skills, commands, prompts, or workflows generated by trellis init."
---
# Trellis Meta
This skill is for local Trellis users who have already run `trellis init` in a project. After reading it, an AI should understand the Trellis architecture, operating model, and customization entry points inside that user project, then modify the generated `.trellis/` and platform directory files according to the user's request.
The default operating scope is local files in the user project:
- `.trellis/`: workflow, config, tasks, spec, workspace, scripts, and runtime state.
- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories.
- Shared skill layer: `.agents/skills/`.
Do not assume the user has the Trellis source repository. Do not default to modifying the global npm install directory or `node_modules`.
## How To Use
1. Read `references/local-architecture/overview.md` first to establish the local Trellis system model.
2. If the request involves a specific AI tool, read `references/platform-files/platform-map.md` and the relevant platform file notes.
3. If the user wants to change behavior, read `references/customize-local/overview.md`, then open the specific customization topic.
4. Before editing, read the actual files in the user project and treat local content as authoritative.
## References
### Local Architecture
- `references/local-architecture/overview.md`: The three-layer local Trellis architecture and customization principles.
- `references/local-architecture/generated-files.md`: Files generated by `trellis init` and their customization boundaries.
- `references/local-architecture/workflow.md`: Phases, routing, and workflow-state blocks in `.trellis/workflow.md`.
- `references/local-architecture/task-system.md`: Task directories, active tasks, JSONL context, and task runtime.
- `references/local-architecture/spec-system.md`: How `.trellis/spec/` is organized and injected.
- `references/local-architecture/workspace-memory.md`: `.trellis/workspace/`, journals, and cross-session memory.
- `references/local-architecture/context-injection.md`: Hooks, sub-agent preludes, and context injection paths.
### Platform Files
- `references/platform-files/overview.md`: How shared `.trellis/` files relate to platform directories.
- `references/platform-files/platform-map.md`: Platform directories and paths for skills, agents, hooks, and extensions.
- `references/platform-files/hooks-and-settings.md`: How settings/config files, hooks, plugins, and extensions connect to Trellis.
- `references/platform-files/agents.md`: Local file responsibilities for `trellis-research`, `trellis-implement`, and `trellis-check`.
- `references/platform-files/skills-and-commands.md`: Differences between skills, commands, prompts, and workflows, plus how to change them.
### Local Customization
- `references/customize-local/overview.md`: Choose the right local customization entry point for the user's request.
- `references/customize-local/change-workflow.md`: Change phases, routing, next actions, and workflow-state.
- `references/customize-local/change-task-lifecycle.md`: Change task creation, status, archive behavior, and hooks.
- `references/customize-local/change-context-loading.md`: Change how tasks, specs, journals, and hook context are loaded.
- `references/customize-local/change-hooks.md`: Change platform hooks, settings, and shell session bridges.
- `references/customize-local/change-agents.md`: Change research, implement, and check agent behavior.
- `references/customize-local/change-skills-or-commands.md`: Add or modify local skills, commands, prompts, and workflows.
- `references/customize-local/change-spec-structure.md`: Adjust the project spec structure under `.trellis/spec/`.
- `references/customize-local/add-project-local-conventions.md`: Put team rules into project-local specs or local skills.
## Current Rules
- `.trellis/workflow.md` is the local workflow source of truth.
- `.trellis/config.yaml` is the project-level Trellis configuration and task hook configuration entry point.
- `.trellis/spec/` stores the user's project-specific coding conventions and design constraints.
- `.trellis/tasks/` stores task PRDs, technical notes, research files, and JSONL context.
- `.trellis/workspace/` stores developer journals and cross-session memory.
- Platform settings/config files decide which hooks, agents, skills, commands, prompts, and workflows actually run.
- `.trellis/.template-hashes.json` and `.trellis/.runtime/` are management/runtime state files. Confirm necessity before editing them.
## Do Not
- Do not treat Trellis upstream source code as the default target for local customization.
- Do not modify the global npm install directory or `node_modules/@mindfoldhq/trellis` to implement project needs.
- Do not overwrite user-modified local files with default templates.
- Do not put team-private project rules into the public `trellis-meta`; put project rules in `.trellis/spec/` or a project-local skill.
- Do not describe removed historical mechanisms as current Trellis behavior.

View File

@@ -0,0 +1,83 @@
# Add Project-Local Conventions
Often the user does not need to change Trellis mechanics; they need local AI to understand their team's conventions. In that case, prefer `.trellis/spec/` or a project-local skill instead of editing `trellis-meta`.
## Where To Put Things
| Content type | Location |
| --- | --- |
| Rules code must follow | `.trellis/spec/<layer>/` |
| Cross-layer thinking methods | `.trellis/spec/guides/` |
| AI capability for a project-specific flow | Platform-local skill |
| One-off task material | `.trellis/tasks/<task>/` |
| Session summary | `.trellis/workspace/<developer>/journal-N.md` |
## Create A Project-Local Skill
If the user wants AI to know "how this project customizes Trellis," create a local skill:
```text
.claude/skills/trellis-local/
└── SKILL.md
```
Example:
```md
---
name: trellis-local
description: "Project-local Trellis customizations for this repository. Use when changing this project's Trellis workflow, hooks, local agents, or team-specific conventions."
---
# Trellis Local
## Local Scope
This skill documents this repository's Trellis customizations only.
## Custom Workflow Rules
- ...
## Local Hook Changes
- ...
## Local Agent Changes
- ...
```
For multi-platform projects, place equivalent versions in other platform skill directories, or use `.agents/skills/` for platforms that support the shared layer.
## Write To `.trellis/spec/`
If the content is a coding convention, write it to spec. Examples:
```text
.trellis/spec/backend/error-handling.md
.trellis/spec/frontend/components.md
.trellis/spec/guides/cross-platform-thinking-guide.md
```
After writing it, update the corresponding `index.md` so AI can find the new rule from the entry point.
## Make The Current Task Use New Conventions
After writing a spec, add it to the current task context:
```bash
python ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/backend/error-handling.md" "Error handling conventions"
python ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/backend/error-handling.md" "Review error handling"
```
## Do Not Store Project-Private Rules In `trellis-meta`
`trellis-meta` is a public skill for understanding Trellis architecture and local customization entry points. Put project-private content in:
- `.trellis/spec/`
- a project-local skill
- the current task
- workspace journal
This prevents future updates to Trellis's built-in `trellis-meta` from overwriting the team's own conventions.

View File

@@ -0,0 +1,54 @@
# Change Local Agents
When the user wants to change `trellis-research`, `trellis-implement`, or `trellis-check` behavior, edit platform agent files in the user project.
## Read These Files First
1. Target platform agent directory
2. `.trellis/workflow.md` Phase 2 / research routing
3. Current task `prd.md`
4. Current task `implement.jsonl` / `check.jsonl`
5. Relevant hook or agent prelude
## Common Paths
| Platform | Path |
| --- | --- |
| Claude Code | `.claude/agents/trellis-*.md` |
| Cursor | `.cursor/agents/trellis-*.md` |
| OpenCode | `.opencode/agents/trellis-*.md` |
| Codex | `.codex/agents/trellis-*.toml` |
| Kiro | `.kiro/agents/trellis-*.json` |
| Gemini CLI | `.gemini/agents/trellis-*.md` |
| Qoder | `.qoder/agents/trellis-*.md` |
| CodeBuddy | `.codebuddy/agents/trellis-*.md` |
| Factory Droid | `.factory/droids/trellis-*.md` |
| Pi Agent | `.pi/agents/trellis-*.md` |
Use the actual paths in the user project as authoritative.
## Common Needs
| Need | Which agent to edit |
| --- | --- |
| Research must write files, not only reply in chat | `trellis-research` |
| Certain local specs must be read before implementation | `trellis-implement` + `implement.jsonl` configuration rules |
| Specific commands must run during checking | `trellis-check` |
| Agent must not modify certain directories | The corresponding agent's write boundary instructions |
| Agent output format must be fixed | The corresponding agent's final/reporting instructions |
## Modification Principles
1. **Preserve role boundaries**: research investigates and persists; implement writes implementation; check reviews and fixes.
2. **Do not hard-code project specs into agents**: long-term specs belong in `.trellis/spec/`; agents are responsible for reading them.
3. **Make read order explicit**: active task -> PRD -> info -> JSONL -> spec/research.
4. **Make write boundaries explicit**: which directories may be written and which may not.
5. **Synchronize across platforms**: when the user configured multiple platforms, decide whether to change only the current platform or all platform agents.
## Agent Pull Platforms
If an agent file contains a prelude for "read task/context after startup," do not remove those steps when editing. Otherwise the agent will work only from chat context and bypass Trellis's core mechanism.
## Hook Push Platforms
If context is injected by a hook, the agent file should still retain responsibility boundaries. Do not remove PRD/spec requirements from the agent just because a hook injects context.

View File

@@ -0,0 +1,81 @@
# Change Local Context Loading
Context loading determines when AI reads workflow, task, spec, research, workspace, and git status. Read this page when the user says "AI does not know the current task," "the agent did not read specs," or "there is too much/too little context."
## Read These Files First
1. `.trellis/workflow.md`
2. `.trellis/scripts/get_context.py`
3. `.trellis/scripts/common/session_context.py`
4. `.trellis/scripts/common/task_context.py`
5. `.trellis/scripts/common/active_task.py`
6. Current platform hooks or agent files
7. The current task's `implement.jsonl` / `check.jsonl`
## Context Sources
| Source | Purpose |
| --- | --- |
| `.trellis/workflow.md` | Workflow and next-action hints. |
| `.trellis/tasks/<task>/prd.md` | Current task requirements. |
| `.trellis/tasks/<task>/implement.jsonl` | Spec/research to read before implementation. |
| `.trellis/tasks/<task>/check.jsonl` | Spec/research to read during checking. |
| `.trellis/spec/` | Project specs. |
| `.trellis/workspace/` | Session records. |
| git status | Current working tree changes. |
## Common Needs And Edit Points
| Need | Edit point |
| --- | --- |
| Inject more/less information in new sessions | `session_context.py` or the platform `session-start` hook. |
| Change hints on each user input | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The `inject-workflow-state` hook is parser-only and reads the block verbatim. |
| Agent did not read specs | Task JSONL, agent prelude, `inject-subagent-context` hook. |
| Active task is lost | `active_task.py` and platform session identity propagation. |
| Change JSONL validation rules | `task_context.py`. |
## JSONL Rules
`implement.jsonl` / `check.jsonl` are the key context loading interface:
```jsonl
{"file": ".trellis/spec/backend/index.md", "reason": "Backend conventions"}
{"file": ".trellis/tasks/04-28-x/research/api.md", "reason": "API research"}
```
Include only spec/research files. Do not put code files that will be modified into these manifests; agents read code files themselves during implementation.
## Change Session Context
If the user wants every new session to see more project state, edit:
- `.trellis/scripts/common/session_context.py`
- the corresponding platform `session-start` hook
Context cannot grow without bound. Prefer injecting indexes and paths so the AI can read detailed files on demand.
## Change Sub-Agent Context
First determine which mode the platform uses:
- hook push: edit the `inject-subagent-context` hook.
- agent pull: edit the read steps in the corresponding `trellis-implement` / `trellis-check` agent file.
In both modes, make sure the agent ultimately reads:
1. active task
2. `prd.md`
3. `info.md` if present
4. the corresponding JSONL
5. spec/research referenced by the JSONL
## Troubleshooting Order
```bash
python ./.trellis/scripts/task.py current --source
python ./.trellis/scripts/task.py list-context <task>
python ./.trellis/scripts/task.py validate <task>
python ./.trellis/scripts/get_context.py --mode packages
```
Confirm the task and JSONL are correct before editing hooks/agents.

View File

@@ -0,0 +1,57 @@
# Change Local Hooks
Hooks are the automation layer that connects a platform to Trellis. When the user wants to change "when context is injected," "how shell commands inherit a session," or "which files are read before an agent starts," hooks are usually the edit point.
## Read These Files First
1. Target platform settings/config, such as `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`
2. Target platform hooks directory
3. `.trellis/scripts/common/active_task.py`
4. `.trellis/scripts/common/session_context.py`
5. `.trellis/workflow.md`
## Common Hook Types
| Hook | Purpose |
| --- | --- |
| session-start | Injects a Trellis overview when a session starts, clears, or compacts. |
| workflow-state | Injects a state hint on each user input. |
| sub-agent context | Injects PRD/spec/research before an agent starts. |
| shell session bridge | Lets `task.py` commands in shell see the same session identity. |
## Modification Steps
1. Find the hook registration in settings/config.
2. Confirm the registered script path exists.
3. Read the hook script and identify inputs, outputs, and called `.trellis/scripts/`.
4. Modify hook behavior.
5. If the hook depends on workflow content, synchronize `.trellis/workflow.md`.
## Example: Change New-Session Injection Content
First find the session-start hook:
```text
.claude/settings.json
.claude/hooks/session-start.py
```
If the hook ultimately calls `.trellis/scripts/get_context.py` or `session_context.py`, editing the local script is usually more robust than hard-coding content in the hook.
## Example: Agent Did Not Read JSONL
First confirm:
```bash
python ./.trellis/scripts/task.py current --source
python ./.trellis/scripts/task.py validate <task>
```
If the task and JSONL are correct, determine whether the platform uses hook push or agent pull. For hook push, edit `inject-subagent-context`; for agent pull, edit the agent file.
## Notes
- Settings handle registration, hook scripts handle behavior; inspect both together.
- Different platforms support different hook events. Do not directly copy another platform's settings.
- Hooks should read project-local `.trellis/`; they should not depend on Trellis upstream source paths.
- Hook failures should produce visible errors so AI does not silently lose context.

View File

@@ -0,0 +1,78 @@
# Change Local Skills, Commands, Prompts, And Workflows
When the user wants to change AI entry points, auto-trigger rules, or explicit command behavior, edit skills, commands, prompts, or workflows in local platform directories.
## Read These Files First
1. `.trellis/workflow.md`
2. Target platform skill/command/prompt/workflow directory
3. Related agent or hook files
4. Whether project rules already exist in `.trellis/spec/`
## Which Entry Type To Choose
| Goal | Recommendation |
| --- | --- |
| AI should automatically know a capability | Add or modify a skill. |
| User wants to trigger manually with a command | Add or modify a command/prompt/workflow. |
| Team project conventions | Prefer `.trellis/spec/` or a project-local skill. |
| Change Trellis flow semantics | Synchronize `.trellis/workflow.md`. |
## Modify A Skill
A skill is usually:
```text
<skill-name>/
├── SKILL.md
└── references/
```
`SKILL.md` should be short and responsible for triggering/routing. Put long content in `references/` so AI can read it on demand.
The frontmatter description should specify when to use the skill. Example:
```yaml
description: "Use when customizing this project's deployment workflow and release checklist."
```
Do not write vague descriptions such as "helpful project skill"; they can trigger incorrectly.
## Modify A Command/Prompt/Workflow
Explicit entry points should state:
- How the user triggers it.
- Which `.trellis/` files to read.
- Which scripts to run.
- How to report after completion.
If a command only repeats workflow rules, prefer making it reference/read `.trellis/workflow.md` instead of maintaining a second copy of the flow.
## Common Paths
| Platform | Entry directories |
| --- | --- |
| Claude Code | `.claude/skills/`, `.claude/commands/` |
| Cursor | `.cursor/skills/`, `.cursor/commands/` |
| OpenCode | `.opencode/skills/`, `.opencode/commands/` |
| Codex | `.agents/skills/`, `.codex/skills/` |
| GitHub Copilot | `.github/skills/`, `.github/prompts/` |
| Kilo / Antigravity / Windsurf | workflows + skills |
## Add A Project-Local Skill
If the user wants to document team-private customizations, create a project-local skill, for example:
```text
.claude/skills/project-trellis-local/
└── SKILL.md
```
For multi-platform projects, add equivalent versions in each platform skill directory, or use `.agents/skills/` on platforms that support the shared layer.
## Notes
- Do not mix every platform's syntax into one file.
- Do not change only one platform entry point while claiming all platforms are supported.
- Do not hide long-term engineering conventions inside a command; write them to `.trellis/spec/`.

View File

@@ -0,0 +1,83 @@
# Change Local Spec Structure
When the user wants to change the engineering conventions AI follows, add new spec layers, or adjust monorepo package mapping, edit `.trellis/spec/` and `.trellis/config.yaml`.
## Read These Files First
1. `.trellis/config.yaml`
2. `.trellis/spec/`
3. `.trellis/workflow.md` Phase 1.3 and Phase 3.3
4. Current task `implement.jsonl` / `check.jsonl`
## Common Needs
| Need | Edit location |
| --- | --- |
| Add backend/frontend/docs/test spec layer | `.trellis/spec/<layer>/` or `.trellis/spec/<package>/<layer>/` |
| Add shared thinking guides | `.trellis/spec/guides/` |
| Adjust monorepo packages | `packages` in `.trellis/config.yaml` |
| Change default package | `default_package` in `.trellis/config.yaml` |
| Control spec scanning scope | `spec_scope` in `.trellis/config.yaml` |
| Make a task read a new spec | Task `implement.jsonl` / `check.jsonl` |
## Add A Spec Layer
Single-repository example:
```text
.trellis/spec/security/
├── index.md
└── auth.md
```
Monorepo example:
```text
.trellis/spec/webapp/security/
├── index.md
└── auth.md
```
`index.md` should include:
- What code this layer applies to.
- Pre-Development Checklist.
- Quality Check.
- Links to specific guideline files.
## Update Context
Adding a spec does not mean every task automatically reads it. The current task must reference it in JSONL:
```bash
python ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/webapp/security/index.md" "Security conventions"
python ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/webapp/security/index.md" "Security review rules"
```
## Change Monorepo Packages
Example `.trellis/config.yaml`:
```yaml
packages:
webapp:
path: apps/web
api:
path: apps/api
default_package: webapp
```
After editing, run:
```bash
python ./.trellis/scripts/get_context.py --mode packages
```
Use this output to confirm AI can see the correct packages and spec layers.
## Notes
- Specs are user project conventions and can be changed according to project needs.
- Do not put temporary task information into specs; put temporary information in the task.
- Do not put long-term conventions only in agents or commands; preserve them in specs.
- After changing spec structure, check whether existing task JSONL files still point to files that exist.

View File

@@ -0,0 +1,90 @@
# Change Local Task Lifecycle
Task lifecycle includes creation, start, context configuration, finish, archive, parent/child tasks, and lifecycle hooks. The default customization targets are `.trellis/tasks/`, `.trellis/config.yaml`, and `.trellis/scripts/`.
## Read These Files First
1. `.trellis/workflow.md`
2. `.trellis/config.yaml`
3. `.trellis/scripts/task.py`
4. `.trellis/scripts/common/task_store.py`
5. `.trellis/scripts/common/task_utils.py`
6. The current task's `.trellis/tasks/<task>/task.json`
## Common Needs And Edit Points
| Need | Edit point |
| --- | --- |
| Automatically sync an external system after task creation | `hooks.after_create` in `.trellis/config.yaml`. |
| Automatically update status after task start | `hooks.after_start` in `.trellis/config.yaml`. |
| Run a script after task finish | `hooks.after_finish` in `.trellis/config.yaml`. |
| Clean external resources after archive | `hooks.after_archive` in `.trellis/config.yaml`. |
| Change default task fields | `.trellis/scripts/common/task_store.py`. |
| Change task parsing/search | `.trellis/scripts/common/task_utils.py`. |
| Change active task behavior | `.trellis/scripts/common/active_task.py`. |
## lifecycle hooks
`.trellis/config.yaml` supports:
```yaml
hooks:
after_create:
- "python .trellis/scripts/hooks/my_sync.py create"
after_start:
- "python .trellis/scripts/hooks/my_sync.py start"
after_finish:
- "python .trellis/scripts/hooks/my_sync.py finish"
after_archive:
- "python .trellis/scripts/hooks/my_sync.py archive"
```
Hook commands receive the `TASK_JSON_PATH` environment variable, pointing to the current task's `task.json`. Hook failures should usually warn, but not block the main task operation.
## Change Task Fields
If the user wants to add project-local fields, prefer putting them under `meta` in `task.json` to avoid breaking existing scripts' assumptions about standard fields.
Example:
```json
"meta": {
"linearIssue": "ENG-123",
"risk": "high"
}
```
If standard fields really need to change, inspect every local script that reads `task.json`.
## Change Active Task
Active task is session-level state stored in `.trellis/.runtime/sessions/`. Do not fall back to a global `.current-task` model. If the user wants to change active task behavior, edit:
- `.trellis/scripts/common/active_task.py`
- platform hooks or shell session bridges
- active task descriptions in `.trellis/workflow.md`
### `task.py create` Sets the Active Pointer
`cmd_create` in `.trellis/scripts/common/task_store.py` calls `set_active_task` best-effort right after writing the new task directory. The behavior:
- When the calling shell carries session identity (`TRELLIS_CONTEXT_ID` env var, or any platform-specific session env that `resolve_context_key` recognizes — see `active_task.py:_ENV_SESSION_KEYS`), the per-session pointer at `.trellis/.runtime/sessions/<context_key>.json` is rewritten to point at the new task. The task's `status=planning` and `[workflow-state:planning]` fires on the very next `UserPromptSubmit`.
- When session identity is unavailable (raw CLI invocation outside an AI session, or a platform that doesn't propagate identity to shell), the task directory is still created and `status=planning` is still written, but the active pointer is left untouched. The user can attach the task later with `task.py start <dir>` once they're back in an AI session.
This makes `[workflow-state:planning]` the live breadcrumb during the brainstorm and JSONL curation work that follows `task.py create`. The pre-R7 behavior left the breadcrumb stuck on `no_task` until `task.py start`, so the planning block was effectively dead text.
If you fork `task.py` to add a new creation path (e.g. an external import that bypasses `cmd_create`), audit whether your path also calls `set_active_task`. Without that call, your created tasks will not surface as active. The full status writer table is in `.trellis/spec/cli/backend/workflow-state-contract.md`.
## Modification Steps
1. Confirm the current task with `python ./.trellis/scripts/task.py current --source`.
2. Read the current task's `task.json` and confirm status and fields.
3. For configuration needs, edit `.trellis/config.yaml` first.
4. For script behavior needs, then edit `.trellis/scripts/`.
5. If the AI flow changed, synchronize `.trellis/workflow.md`.
## Do Not
- Do not directly edit `.trellis/.runtime/sessions/` to "fix" business state.
- Do not hard-code project-private fields into scripts; prefer `meta`.
- Do not default to asking the user to fork Trellis CLI.

View File

@@ -0,0 +1,64 @@
# Change Local Workflow
When the user wants to change Trellis phases, next-action hints, whether to create tasks, whether to use sub-agents, or when to check/wrap up, edit `.trellis/workflow.md` first.
## Read These Files First
1. `.trellis/workflow.md`
2. Entry files for the current platform, such as skills/commands/prompts/workflows
3. The current task's `task.json` and `prd.md`
## Common Needs And Edit Points
| Need | Edit point |
| --- | --- |
| Change phase names or phase order | `Phase Index` and the corresponding Phase sections. |
| Change whether to create a task when there is no task | `[workflow-state:no_task]` state block. |
| Change the next step during planning | Phase 1 and `[workflow-state:planning]`. |
| Change whether an agent is required during in_progress | Phase 2 and `[workflow-state:in_progress]`. |
| Change wrap-up after completion | Phase 3 and `[workflow-state:completed]`. |
| Change which skill a user intent triggers | `Skill Routing` table. |
## Modification Steps
1. Find the relevant section in `.trellis/workflow.md`.
2. When changing rules, keep explicit trigger conditions and next actions.
3. If adding or renaming a skill/agent, synchronize the corresponding files in platform directories.
4. Workflow-state changes only need an edit to the `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook is parser-only — it reads whatever you put in the block. Keep the opening and closing tags' STATUS strings identical (`[workflow-state:foo]…[/workflow-state:foo]`); mismatched STATUS pairs are silently dropped.
5. Make the AI reread `.trellis/workflow.md`; do not keep using rules from the old conversation.
## Example: Relax Task Creation Requirements
To change when task creation can be skipped, usually edit `[workflow-state:no_task]`:
```md
[workflow-state:no_task]
Task is not required when the answer is a one-reply explanation, no files are changed, and no research is needed.
[/workflow-state:no_task]
```
If the formal Phase 1 flow also needs to change, synchronize the Phase 1 section.
## Example: One Platform Does Not Use Sub-Agents
If the user wants only one platform to avoid sub-agents, first confirm whether that platform has a separate group in the workflow. Then change Phase 2 routing for that platform group instead of deleting all `trellis-implement` / `trellis-check` instructions across platforms.
## `/trellis:continue` Route Table
`/trellis:continue` resumes a task by deciding which phase step to load next. The decision combines `task.json.status` with the presence of artifacts inside the task directory. The mapping is fixed in the command itself; forks that add custom statuses must extend both the workflow.md tag block and this table.
| `status` | Artifact state | Resume at |
| --- | --- | --- |
| `planning` | `prd.md` missing | Phase 1.1 (load `trellis-brainstorm`) |
| `planning` | `prd.md` exists, `implement.jsonl` only has the seed `_example` row | Phase 1.3 (curate JSONL context) |
| `planning` | `prd.md` exists, `implement.jsonl` curated | Phase 1.4 (run `task.py start`) |
| `in_progress` | no implementation in conversation history | Phase 2.1 (`trellis-implement`) |
| `in_progress` | implementation done, no `trellis-check` run | Phase 2.2 (`trellis-check`) |
| `in_progress` | check passed | Phase 3.1 (verify quality + spec update) |
| `completed` | task is still in active tree | Phase 3.5 (run `/trellis:finish-work` to archive) |
When you add a custom status (e.g. `in-review`), add a `[workflow-state:in-review]` block in `.trellis/workflow.md` for the per-turn breadcrumb AND extend this route table — usually by editing the `/trellis:continue` command file (`.{platform}/commands/trellis/continue.md` or equivalent) to add a row that decides where to resume from. Without the route entry, `/trellis:continue` will fall through to a default branch and the user will not land on the step you intended.
## Notes
`.trellis/workflow.md` is the local project workflow, not an immutable template. The user can adapt it to team habits. After editing it, platform entry files may still contain old descriptions, so inspect them too.

View File

@@ -0,0 +1,55 @@
# Local Customization Overview
This directory is for local AI working in a user project where Trellis was installed through npm and `trellis init` has already been run. The AI should modify generated `.trellis/` and platform directories inside the project, not Trellis CLI upstream source code.
## First Determine What The User Actually Wants To Change
| User wording | Read first |
| --- | --- |
| "Change the Trellis flow / phases / next prompt" | `change-workflow.md` |
| "Change task creation, status, archive, or hooks" | `change-task-lifecycle.md` |
| "AI did not read context / change injected content" | `change-context-loading.md` |
| "A platform hook is not behaving as expected" | `change-hooks.md` |
| "Change implement/check/research agent behavior" | `change-agents.md` |
| "Add a skill/command/workflow/prompt" | `change-skills-or-commands.md` |
| "Adjust the project spec structure" | `change-spec-structure.md` |
| "Add team conventions and local notes" | `add-project-local-conventions.md` |
## General Operation Order
1. **Confirm platform and directories**: inspect which directories exist, such as `.claude/`, `.codex/`, `.cursor/`.
2. **Confirm the current active task**: run `python ./.trellis/scripts/task.py current --source`.
3. **Read the local source of truth**: prefer `.trellis/workflow.md`, `.trellis/config.yaml`, and relevant platform files.
4. **Modify narrowly**: edit only files related to the user's request.
5. **Synchronize semantics**: if a shared flow changes, check whether platform entry points also need changes; if a platform entry changes, check whether `.trellis/workflow.md` still agrees.
## Local File Priority
| Layer | Files |
| --- | --- |
| Workflow | `.trellis/workflow.md` |
| Project configuration | `.trellis/config.yaml` |
| Task material | `.trellis/tasks/<task>/` |
| Project specs | `.trellis/spec/` |
| Runtime scripts | `.trellis/scripts/` |
| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, and similar directories |
| Shared skill | `.agents/skills/` |
## Things Not To Do By Default
- Do not edit the global npm install directory.
- Do not edit `node_modules/@mindfoldhq/trellis`.
- Do not assume the user has the Trellis GitHub repository.
- Do not overwrite local files already modified by the user with default templates.
- Do not put team project rules into public `trellis-meta`; project rules belong in `.trellis/spec/` or a local skill.
## When To Inspect Upstream Source
Switch to an upstream source-code perspective only when the user explicitly expresses one of these goals:
- "I want to open a PR to Trellis"
- "I want to change npm package publish contents"
- "I want to fork Trellis"
- "I want to modify the generation logic for `trellis init/update`"
Otherwise, default to modifying local Trellis files inside the user project.

View File

@@ -0,0 +1,68 @@
# Local Context Injection System
Trellis context injection aims to make AI read the right files at the right time instead of relying on model memory. In a user project, injection is implemented by `.trellis/` scripts together with platform hooks, agents, and skills.
## Injected Context Types
| Type | Source | Purpose |
| --- | --- | --- |
| session context | `.trellis/scripts/get_context.py` | Current developer, git status, active task, active tasks, journal, packages. |
| workflow context | `.trellis/workflow.md` | Current Trellis flow and next action. |
| spec context | `.trellis/spec/` + task JSONL | Specs that must be followed during implementation/checking. |
| task context | `.trellis/tasks/<task>/prd.md`, `info.md`, `research/` | Current task requirements, design, and research. |
| platform context | Platform hooks/settings/agents | Lets different AI tools read the files above through their own mechanisms. |
## session-start
Platforms with session-start support inject a Trellis overview when a session starts, clears, compacts, or receives a similar event. Injected content usually includes:
- workflow summary.
- current task status.
- active tasks.
- spec index paths.
- developer identity and git status.
If the user feels the AI does not know the current task in a new session, first check whether the platform's session-start hook or equivalent mechanism is installed and running.
## workflow-state
workflow-state is a lightweight hint injected around each user turn. Based on current task status, it selects a block from `.trellis/workflow.md`, such as `no_task`, `planning`, `in_progress`, or `completed`.
If the user wants to change "what the AI should do next in a given state," edit the corresponding state block in `.trellis/workflow.md` first.
## sub-agent context
Implement and check agents need task context. Trellis has two loading modes:
1. **hook push**: a platform hook injects `prd.md` and the files referenced by `implement.jsonl` / `check.jsonl` before the agent starts.
2. **agent pull**: the agent definition instructs the agent to read the active task, PRD, and JSONL context after startup.
In both modes, JSONL files in the task directory are the key interface.
## JSONL Reading Rules
`implement.jsonl` and `check.jsonl` contain one JSON object per line:
```jsonl
{"file": ".trellis/spec/backend/index.md", "reason": "Backend rules"}
```
Readers should skip seed rows without a `file` field. When configuring JSONL, the AI should include only spec/research files, not pre-register code files that will be modified.
## Active Task And Context Key
Active task state lives in `.trellis/.runtime/sessions/` and is isolated per session. Hooks try to resolve the context key from platform events, environment variables, transcript paths, or `TRELLIS_CONTEXT_ID`.
If shell commands cannot see the same context key, `task.py current --source` may report no active task. In that case, check whether the platform passes session identity into the shell instead of hand-writing a global current-task file.
## Local Customization Points
| Need | Edit location |
| --- | --- |
| Change session-start injected content | The platform's `session-start` hook or plugin file. |
| Change per-turn workflow-state rules | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The platform workflow-state hook parses these blocks verbatim and embeds no fallback text. |
| Change how sub-agents read context | Platform agent definitions, the `inject-subagent-context` hook, or agent preludes. |
| Change JSONL validation/display | `.trellis/scripts/common/task_context.py`. |
| Change active task resolution | `.trellis/scripts/common/active_task.py`. |
When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct PRD/spec/research.

View File

@@ -0,0 +1,80 @@
# Local Files Generated After Init
`trellis init` writes the Trellis runtime into the user project. Later, `trellis update` tries to update Trellis-managed template files, but it uses `.trellis/.template-hashes.json` to determine which files have already been modified by the user.
This page only describes files that are visible and editable inside the user project.
## `.trellis/`
```text
.trellis/
├── workflow.md
├── config.yaml
├── .developer
├── .version
├── .template-hashes.json
├── .runtime/
├── scripts/
├── spec/
├── tasks/
└── workspace/
```
| Path | Usually editable? | Notes |
| --- | --- | --- |
| `.trellis/workflow.md` | Yes | Local workflow documentation and AI routing rules. |
| `.trellis/config.yaml` | Yes | Project configuration, hooks, packages, journal line limits, and related settings. |
| `.trellis/spec/` | Yes | Project specs, intended to be updated regularly by users and AI. |
| `.trellis/tasks/` | Yes | Task material and research artifacts, maintained by the task workflow. |
| `.trellis/workspace/` | Yes | Session records, usually written by `add_session.py`. |
| `.trellis/scripts/` | Carefully | Local runtime. It can be customized, but only after understanding the call chain. |
| `.trellis/.runtime/` | No | Runtime state, usually written automatically by hooks/scripts. |
| `.trellis/.developer` | Carefully | Current developer identity. |
| `.trellis/.version` | No | Trellis version record used by update/migration logic. |
| `.trellis/.template-hashes.json` | No | Template hash record. Do not hand-write business rules here. |
## Platform Directories
Different platforms generate different directories. Common categories:
| Category | Example paths | Purpose |
| --- | --- | --- |
| hooks | `.claude/hooks/`, `.codex/hooks/`, `.cursor/hooks/` | Inject session context, workflow-state, and sub-agent context. |
| settings | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json` | Tell the platform when to run hooks or plugins. |
| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/` | Define agents such as `trellis-research`, `trellis-implement`, and `trellis-check`. |
| skills | `.claude/skills/`, `.agents/skills/`, `.qoder/skills/` | Skills that auto-trigger or can be read by AI. |
| commands/prompts/workflows | `.cursor/commands/`, `.github/prompts/`, `.windsurf/workflows/` | Explicit user-invoked command or workflow entry points. |
When modifying a platform directory, also confirm whether `.trellis/workflow.md` still describes the same flow.
## Meaning Of Template Hashes
`.trellis/.template-hashes.json` records the content hash from the last time Trellis wrote a template file. `trellis update` uses it to distinguish three cases:
| Case | Update behavior |
| --- | --- |
| File was not modified by the user | It can be updated automatically. |
| File was modified by the user | Prompt the user to overwrite, keep, or generate `.new`. |
| File is no longer a current template | It may be deleted, renamed, or preserved according to migration rules. |
When an AI customizes local Trellis files, it does not need to maintain hashes manually. It is normal for Trellis update to recognize the result as "modified by the user."
## Local Customization Boundaries
Editable by default:
- `.trellis/workflow.md`
- `.trellis/config.yaml`
- `.trellis/spec/**`
- `.trellis/scripts/**`
- Platform hooks, settings, agents, skills, commands, prompts, and workflows
Do not edit by default:
- Global npm install directory
- `node_modules/@mindfoldhq/trellis`
- Trellis GitHub repository source code
- Concrete state files under `.trellis/.runtime/**`
- Hash contents inside `.trellis/.template-hashes.json`
Switch to the Trellis CLI source-code perspective only when the user explicitly wants to contribute upstream.

View File

@@ -0,0 +1,51 @@
# Local Trellis Architecture Overview
`trellis-meta` is for user projects that have already run `trellis init`. The user's machine usually has only the npm-installed `trellis` command plus the Trellis files generated inside the project; it may not have the Trellis CLI source code.
Therefore, when an AI uses this skill, the default customization target is local files inside the user project:
- `.trellis/`: workflow, tasks, specs, memory, scripts, and runtime state.
- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories.
- Shared skill layer: `.agents/skills/`.
Do not default to guiding the user to fork the Trellis CLI repository. Treat upstream source code as the operating target only when the user explicitly says they want to change Trellis upstream source, publish an npm package, or contribute a PR.
## Local System Model
Trellis provides three layers inside a user project:
1. **Workflow layer**: `.trellis/workflow.md` defines phases, routing, next actions, and prompt blocks.
2. **Persistence layer**: `.trellis/tasks/`, `.trellis/spec/`, and `.trellis/workspace/` store tasks, specs, and session memory.
3. **Platform integration layer**: hooks, settings, agents, skills, commands, prompts, and workflows in platform directories connect the Trellis workflow to different AI tools.
All three layers live inside the user project, so an AI can read and modify them directly.
## Core Paths
| Path | Purpose |
| --- | --- |
| `.trellis/workflow.md` | Workflow phases, skill routing, and workflow-state prompt blocks. |
| `.trellis/config.yaml` | Project configuration, task lifecycle hooks, monorepo package configuration, and journal configuration. |
| `.trellis/spec/` | The user's project-specific coding conventions and thinking guides. |
| `.trellis/tasks/` | Each task's PRD, technical notes, research files, and JSONL context. |
| `.trellis/workspace/` | Per-developer journals and cross-session memory. |
| `.trellis/scripts/` | Local Python runtime used by commands, hooks, and context injection. |
| `.trellis/.runtime/` | Session-level runtime state, such as the current task pointer. |
| `.trellis/.template-hashes.json` | Template hashes for Trellis-managed files, used by update to determine whether local files were modified by the user. |
## AI Customization Principles
1. **Find the local source of truth first**: Do not edit from memory. Read `.trellis/workflow.md`, `.trellis/config.yaml`, the relevant platform directory, and related task files first.
2. **Edit the user project, not the npm package cache**: Modify generated files inside the project, not `node_modules` or the global npm install directory.
3. **Keep platform files aligned with `.trellis/`**: If workflow routing changes, also check whether platform skills or commands still describe the same flow.
4. **Put project-specific rules in `.trellis/spec/` or a local skill**: Do not put team conventions into `trellis-meta`.
5. **Preserve user changes**: If a file was already modified locally, work from the current content instead of overwriting it with a default template.
## How To Use This Directory
- To understand which files exist after init, read `generated-files.md`.
- To change phases, routing, or next actions, read `workflow.md`.
- To change the task model, JSONL context, or active task behavior, read `task-system.md`.
- To change coding convention injection, read `spec-system.md`.
- To understand journals and cross-session memory, read `workspace-memory.md`.
- To change hooks or sub-agent context loading, read `context-injection.md`.

View File

@@ -0,0 +1,102 @@
# Local Spec System
`.trellis/spec/` is the user's project-specific engineering spec library. Trellis is not about making AI memorize conventions; it injects relevant specs or requires the AI to read them at the right time.
## Directory Model
A common single-repository structure:
```text
.trellis/spec/
├── backend/
│ ├── index.md
│ └── ...
├── frontend/
│ ├── index.md
│ └── ...
└── guides/
├── index.md
└── ...
```
A common monorepo structure:
```text
.trellis/spec/
├── cli/
│ ├── backend/
│ │ ├── index.md
│ │ └── ...
│ └── unit-test/
│ ├── index.md
│ └── ...
├── docs-site/
│ └── docs/
│ ├── index.md
│ └── ...
└── guides/
├── index.md
└── ...
```
`index.md` is the entry point for each layer. It should list the Pre-Development Checklist and Quality Check. Specific guidelines live in other Markdown files in the same directory.
## Package Configuration
`.trellis/config.yaml` can declare packages:
```yaml
packages:
cli:
path: packages/cli
docs-site:
path: docs-site
type: submodule
default_package: cli
```
The AI can run:
```bash
python ./.trellis/scripts/get_context.py --mode packages
```
This command lists packages and spec layers for the current project. Use this output as the reference when configuring context JSONL.
## How Specs Enter Tasks
Before a task enters implementation, Phase 1.3 should write relevant specs into `implement.jsonl` / `check.jsonl`:
```jsonl
{"file": ".trellis/spec/cli/backend/index.md", "reason": "CLI backend conventions"}
{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Test expectations"}
```
Sub-agents or platform preludes read these JSONL files and load the referenced specs. On platforms without sub-agent support, the AI should read the relevant specs directly according to the workflow.
## What Specs Should Contain
Specs should contain executable engineering conventions for the project, not generic best practices:
- Where files should live.
- How error handling should be expressed.
- Input/output contracts for APIs, hooks, and commands.
- Patterns that are forbidden.
- Cases that require tests.
- Project-specific pitfalls and how to avoid them.
When the AI learns a new rule during implementation or debugging, it should update `.trellis/spec/` rather than only summarizing it in chat.
## Local Customization Points
| Need | Edit location |
| --- | --- |
| Add a new spec layer | `.trellis/spec/<package>/<layer>/index.md` and corresponding guideline files. |
| Change monorepo spec mapping | `packages` / `default_package` / `spec_scope` in `.trellis/config.yaml`. |
| Change which specs AI reads before implementation | The task's `implement.jsonl`. |
| Change which specs AI reads during checking | The task's `check.jsonl`. |
| Change when specs should be updated | Phase 3.3 in `.trellis/workflow.md` and the `trellis-update-spec` skill. |
## Boundaries
`.trellis/spec/` is the user's project specification, not a permanent copy of Trellis built-in templates. The AI should encourage the user to update it according to the actual project code instead of treating Trellis default templates as immutable documents.

View File

@@ -0,0 +1,101 @@
# Local Task System
The Trellis task system is stored entirely under `.trellis/tasks/` in the user project. Each task is a directory containing requirements, context, research, state, and relationship information.
## Task Directory Structure
```text
.trellis/tasks/
├── 04-28-example-task/
│ ├── task.json
│ ├── prd.md
│ ├── info.md
│ ├── implement.jsonl
│ ├── check.jsonl
│ └── research/
└── archive/
└── 2026-04/
```
| File | Purpose |
| --- | --- |
| `task.json` | Task metadata: status, assignee, priority, branch, parent/child tasks, and similar fields. |
| `prd.md` | Requirements document; the most important business context during implementation. |
| `info.md` | Optional technical design. |
| `implement.jsonl` | List of spec/research files the implement agent must read first. |
| `check.jsonl` | List of spec/research files the check agent must read first. |
| `research/` | Research artifacts. Complex findings should not live only in chat. |
## `task.json`
`task.json` records task status and metadata. Common fields:
| Field | Meaning |
| --- | --- |
| `id` / `name` / `title` | Task identity and title. |
| `status` | Status such as `planning`, `in_progress`, `review`, or `completed`. |
| `priority` | `P0`, `P1`, `P2`, `P3`. |
| `creator` / `assignee` | Creator and assignee. |
| `package` | Target package in a monorepo; may be empty. |
| `branch` / `base_branch` | Working branch and PR target branch. |
| `children` / `parent` | Parent/child task relationships. |
| `commit` / `pr_url` | Commit and PR information after completion. |
| `meta` | Extension fields. |
The AI should not treat phase numbers as task status. Task progress is mainly determined by `status`, `prd.md`, whether JSONL context is configured, and the phase descriptions in `workflow.md`.
## Active Task
The user sees a "current task," but Trellis stores active task state per session.
```text
.trellis/.runtime/sessions/<context-key>.json
```
`task.py start` writes the task path into the runtime session file for the current session. `task.py current --source` shows the current task and where it came from. Different AI windows can point to different tasks without overwriting each other.
If the platform or shell environment has no stable session identity, `task.py start` may be unable to set the active task. The AI should read the error, inspect the platform hook/session environment, and not fall back to a shared global pointer.
## JSONL Context
`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first.
Format:
```jsonl
{"file": ".trellis/spec/cli/backend/index.md", "reason": "Backend conventions"}
{"file": ".trellis/tasks/04-28-example/research/api.md", "reason": "API research"}
```
Rules:
- Include spec and research files.
- Do not include code files that are about to be modified.
- Do not treat temporary conclusions in chat as the only context.
- Seed rows have no `file` field; they only prompt the AI to fill in real entries.
## Common Commands
```bash
python ./.trellis/scripts/task.py create "<title>" --slug <slug>
python ./.trellis/scripts/task.py start <task>
python ./.trellis/scripts/task.py current --source
python ./.trellis/scripts/task.py add-context <task> implement <file> <reason>
python ./.trellis/scripts/task.py validate <task>
python ./.trellis/scripts/task.py finish
python ./.trellis/scripts/task.py archive <task>
```
When modifying the task system, the AI should prefer script commands to maintain structure. Edit JSON/Markdown directly only when scripts do not cover the need.
## Local Customization Points
| Need | Edit location |
| --- | --- |
| Change the default task template | `.trellis/scripts/common/task_store.py` and task creation instructions. |
| Change status semantics | `.trellis/workflow.md`, workflow-state hook logic, and task usage conventions. |
| Add task lifecycle actions | `hooks.after_*` in `.trellis/config.yaml`. |
| Change context rules | Phase 1.3 in `.trellis/workflow.md` and related platform agent/hook instructions. |
| Change archive policy | `.trellis/scripts/common/task_store.py` / `task_utils.py`. |
These are local files in the user project. Do not default to editing Trellis CLI source code unless the user wants to contribute upstream.

View File

@@ -0,0 +1,75 @@
# Local Workflow System
`.trellis/workflow.md` is the Trellis workflow source of truth inside the user project. An AI does not need Trellis source code to understand how the current project should move tasks forward; this file is enough.
## File Responsibilities
`.trellis/workflow.md` has three responsibilities:
1. **Explain workflow phases**: Plan, Execute, Finish.
2. **Define skill routing**: which skill or agent the AI should use when the user expresses a certain intent.
3. **Provide workflow-state prompt blocks**: hooks can inject the prompt block for the current state into the conversation.
## Current Phase Model
```text
Phase 1: Plan -> clarify what to build, produce prd.md and required research
Phase 2: Execute -> implement against the PRD and specs, then check
Phase 3: Finish -> final verification, preserve lessons, and wrap up
```
Each phase contains numbered steps, such as `1.3 Configure context`. These numbers are not runtime fields in `task.json`; they are workflow structure for AI and humans to read.
## Skill Routing
`workflow.md` separates routing by platform capability:
- Platforms with sub-agent support: dispatch `trellis-implement` by default for implementation and `trellis-check` for checking.
- Platforms without sub-agent support: the main session reads skills such as `trellis-before-dev`, then executes directly.
When changing local AI behavior, update the routing descriptions in `workflow.md` first, then check whether the corresponding platform skill, command, or agent files need to stay in sync.
## Workflow-State Prompt Blocks
The bottom of `workflow.md` can contain state blocks like this:
```text
[workflow-state:no_task]
...
[/workflow-state:no_task]
```
Hooks choose the right block based on current task status and inject it into the conversation. Common states include:
| State | Meaning |
| --- | --- |
| `no_task` | The current session has no active task. |
| `planning` | The task is still in requirements, research, or context configuration. |
| `in_progress` | The task has entered implementation and checking. |
| `completed` | The task is complete and waiting for wrap-up or archive. |
If the user wants to change policies such as "whether to create a task when there is no task," "when task creation may be skipped," or "whether sub-agents are required," edit these state blocks and the routing table above them.
## Local Modification Patterns
Common changes:
| Goal | Edit point |
| --- | --- |
| Add a phase | Update the Phase Index, phase body, routing, and state blocks. |
| Change task creation policy | Update the `no_task` state block and Phase 1 description. |
| Change the default implementation/check path | Update Phase 2 and skill routing. |
| Change the wrap-up flow | Update Phase 3 and `finish-work` related descriptions. Note the current split: Phase 3.4 = AI-driven code commits (batched, user-confirmed), Phase 3.5 = `/finish-work` (archive + record session). `/finish-work` refuses to run if the working tree is dirty. |
| Change platform differences | Update routing descriptions grouped by platform. |
After editing, make the AI reread `.trellis/workflow.md`; do not assume the flow from the old conversation is still valid.
## Relationship To Platform Files
`workflow.md` is the semantic center of the local workflow, but each platform can also have its own entry files:
- skills, such as `trellis-brainstorm` and `trellis-check`.
- commands/prompts/workflows, such as continue and finish-work.
- hooks, such as session-start or workflow-state injection.
If only `workflow.md` changes, platform entry files may still contain old language. When the user wants to change "what the AI actually does," also inspect the relevant platform directory.

View File

@@ -0,0 +1,71 @@
# Local Workspace Memory System
`.trellis/workspace/` stores cross-session memory. Its purpose is to let AI and humans understand what happened before across different windows and different days.
## Directory Structure
```text
.trellis/workspace/
├── index.md
└── <developer>/
├── index.md
├── journal-1.md
└── journal-2.md
```
| File | Purpose |
| --- | --- |
| `.trellis/.developer` | Current developer identity. |
| `.trellis/workspace/index.md` | Global workspace overview. |
| `.trellis/workspace/<developer>/index.md` | Session index for a developer. |
| `.trellis/workspace/<developer>/journal-N.md` | Session journal. |
## Developer Identity
Run this the first time:
```bash
python ./.trellis/scripts/init_developer.py <name>
```
This creates `.trellis/.developer` and the corresponding workspace directory. The AI should not change developer identity casually; if the identity is wrong, first confirm who is using the current project.
## Journal
`journal-N.md` records completed or partially completed work from each session. By default, each journal holds about 2000 lines; after that it rotates to the next file.
Common command for recording a session:
```bash
python ./.trellis/scripts/add_session.py \
--title "Session title" \
--summary "What changed" \
--commit "abc1234"
```
Planning or review work without a commit can also be recorded by using `--no-commit` or an empty commit value.
## Relationship Between Workspace Memory And Tasks
| System | What it stores |
| --- | --- |
| `.trellis/tasks/` | Requirements, design, research, and state for a specific task. |
| `.trellis/workspace/` | Work records across tasks and sessions. |
| `.trellis/spec/` | Engineering knowledge preserved as long-term conventions. |
If information is only useful for the current task, put it in the task directory.
If information describes what happened in the current session, put it in the workspace journal.
If information should be followed every time code is written in the future, put it in spec.
## Local Customization Points
| Need | Edit location |
| --- | --- |
| Change maximum journal lines | `max_journal_lines` in `.trellis/config.yaml`. |
| Change session auto-commit message | `session_commit_message` in `.trellis/config.yaml`. |
| Change session content format | `.trellis/scripts/add_session.py`. |
| Change how workspace is displayed in context | `.trellis/scripts/common/session_context.py`. |
## AI Usage Rules
The AI should not treat workspace as the only source of truth. When resuming a task, read the current task first, then use workspace for background. After a task is complete, record important process notes in workspace; if long-term rules emerged, update spec.

View File

@@ -0,0 +1,79 @@
# Agents
Trellis agent files define specialized roles. Common Trellis agents in a user project are:
- `trellis-research`
- `trellis-implement`
- `trellis-check`
File locations and formats differ by platform, but responsibility boundaries should stay consistent.
## Agent Responsibilities
| Agent | Responsibility |
| --- | --- |
| `trellis-research` | Investigate the question and write findings into the current task's `research/`. |
| `trellis-implement` | Implement against `prd.md`, `info.md`, `implement.jsonl`, and related spec/research. |
| `trellis-check` | Review changes, fix discovered issues, and run necessary checks. |
Agent files should not become generic chat prompts. They should define input sources, write boundaries, whether code may be changed, and how results are reported.
## Common Paths
| Platform | Agent path |
| --- | --- |
| Claude Code | `.claude/agents/trellis-*.md` |
| Cursor | `.cursor/agents/trellis-*.md` |
| OpenCode | `.opencode/agents/trellis-*.md` |
| Codex | `.codex/agents/trellis-*.toml` |
| Kiro | `.kiro/agents/trellis-*.json` |
| Gemini CLI | `.gemini/agents/trellis-*.md` |
| Qoder | `.qoder/agents/trellis-*.md` |
| CodeBuddy | `.codebuddy/agents/trellis-*.md` |
| Factory Droid | `.factory/droids/trellis-*.md` |
| Pi Agent | `.pi/agents/trellis-*.md` |
GitHub Copilot agent/prompt support is provided by a combination of directories such as `.github/agents/`, `.github/prompts/`, and `.github/skills/`; inspect the files actually generated in the user project.
Main-session workflow platforms such as Kilo, Antigravity, and Windsurf may not have Trellis sub-agent files. They usually rely on workflows/skills to guide the main session.
## Two Context Loading Modes
### hook push
The platform hook injects task context before the agent starts. The agent file itself can focus more on responsibilities and boundaries.
Common on platforms that support agent hooks.
### agent pull
The agent file instructs the agent to read after startup:
- `python ./.trellis/scripts/task.py current --source`
- current task `prd.md`
- `info.md`
- `implement.jsonl` or `check.jsonl`
- spec/research files referenced by JSONL
This mode fits platforms whose hooks cannot reliably rewrite sub-agent prompts.
## Local Change Scenarios
| User need | Edit location |
| --- | --- |
| Implement agent must follow extra restrictions | The platform's `trellis-implement` agent file. |
| Check agent must run project-specific commands | `trellis-check` agent file, and `.trellis/spec/` if needed. |
| Research agent must output a fixed format | `trellis-research` agent file. |
| Agent cannot read task context | Agent prelude or `inject-subagent-context` hook. |
| Add a project-specific agent | Platform agent directory + related workflow/command/skill entry point. |
## Modification Principles
1. **Keep responsibilities single-purpose**. Do not mix research, implement, and check responsibilities into one agent.
2. **Specify the read order**. Agents must know to start from the active task and then find the PRD and JSONL.
3. **Specify write boundaries**. Research usually only writes `research/`; implement can write code; check can fix issues.
4. **Keep semantics synchronized in multi-platform projects**. If the user configured Claude, Codex, and Cursor together, decide whether changes to one platform's agent also need to be applied to others.
## Do Not Default To Editing Upstream Templates
Local AI should default to modifying platform agent files inside the user project. Discuss upstream template source only when the user explicitly wants to contribute the change back to Trellis.

View File

@@ -0,0 +1,69 @@
# Hooks And Settings
Hooks/settings are the entry layer that connects a platform to Trellis. They decide which scripts, plugins, or extensions a platform runs for which events.
## Settings Responsibilities
settings/config files usually register:
- session-start hook: injects a Trellis overview when a new session starts or context resets.
- workflow-state hook: parses `[workflow-state:STATUS]` blocks from `.trellis/workflow.md` and emits the body matching the current task `status` on each user input. Parser-only; the script does not embed fallback content.
- sub-agent context hook: injects task context when implementation/check/research agents start.
- shell/session bridge: lets shell commands see the same Trellis session identity.
- platform plugin or extension entry points.
Common files:
| Platform | settings/config |
| --- | --- |
| Claude Code | `.claude/settings.json` |
| Cursor | `.cursor/hooks.json` |
| Codex | `.codex/hooks.json`, `.codex/config.toml` |
| OpenCode | `.opencode/package.json`, `.opencode/plugins/*` |
| Kiro | `.kiro/hooks/` + platform config |
| Gemini CLI | `.gemini/settings.json` |
| Qoder | `.qoder/settings.json` |
| CodeBuddy | `.codebuddy/settings.json` |
| GitHub Copilot | `.github/copilot/hooks.json` |
| Factory Droid | `.factory/settings.json` |
| Pi Agent | `.pi/settings.json`, `.pi/extensions/trellis/` |
Whether these files exist in a project depends on which `trellis init --<platform>` flags the user ran.
## Hook Script Types
| Script | Purpose |
| --- | --- |
| `session-start.py` | Generates session-start context. |
| `inject-workflow-state.py` | Parses `[workflow-state:STATUS]` blocks in `.trellis/workflow.md` and emits the body matching the current task status. Falls back to `Refer to workflow.md for current step.` when no matching block exists. |
| `inject-subagent-context.py` | Injects PRD, JSONL context, and related spec/research into sub-agents. |
| `inject-shell-session-context.py` | Lets shell commands inherit Trellis session identity. |
Not every platform has every hook. Do not copy files from another platform just because a platform lacks a hook; first confirm whether that platform supports the corresponding event.
## Local Change Scenarios
| User need | Edit location |
| --- | --- |
| AI should see more/less context in a new session | Platform `session-start` hook. |
| Per-turn hint policy should change | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook parses workflow.md verbatim — no script edit required. |
| Sub-agent cannot read PRD/spec | `inject-subagent-context` hook or agent prelude. |
| `task.py current` in shell has no active task | Shell/session bridge hook or platform environment variable configuration. |
| Disable an automatic injection | The corresponding hook registration in settings/config. |
## Modification Principles
1. **Settings wire things up; hooks define behavior**. If only the hook changes, the platform may never call it. If only settings change, behavior may not change.
2. **Confirm platform event names first**. Different platforms use different names for SessionStart, UserPromptSubmit, AgentSpawn, shell execution, and similar events.
3. **Hooks read local `.trellis/`, not upstream source**. `.trellis/scripts/` and `.trellis/workflow.md` in the user project are the default targets.
4. **Errors must be visible**. Hook failures should tell the user what was not injected instead of silently leaving the AI without context.
## Troubleshooting Path
If the user says "AI did not read Trellis state":
1. Check whether the platform settings register the hook.
2. Check whether the hook file exists.
3. Manually run the `.trellis/scripts/get_context.py` or `task.py current --source` command that the hook depends on.
4. Check whether active task state exists in `.trellis/.runtime/sessions/`.
5. Check whether the platform shell passes session identity.

View File

@@ -0,0 +1,59 @@
# Platform Files Overview
Trellis connects the same local architecture to different AI tools. `.trellis/` stores the shared runtime; platform directories store adapter files that define how each AI tool enters Trellis.
When a local AI modifies Trellis, it should distinguish two file categories first:
- **Shared files**: `.trellis/workflow.md`, `.trellis/tasks/`, `.trellis/spec/`, `.trellis/scripts/`.
- **Platform files**: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories.
Platform files do not store business state. They let the corresponding AI tool read Trellis state, call Trellis scripts, and load Trellis skills/agents/hooks.
## Platform File Categories
| Category | Common paths | Purpose |
| --- | --- | --- |
| settings/config | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json` | Register hooks, plugins, extensions, or platform behavior. |
| hooks/plugins/extensions | `.claude/hooks/`, `.opencode/plugins/`, `.pi/extensions/` | Inject context at session start, user input, agent startup, shell execution, and similar events. |
| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/` | Define `trellis-research`, `trellis-implement`, and `trellis-check`. |
| skills | `.claude/skills/`, `.agents/skills/`, `.qoder/skills/` | Capability descriptions that auto-trigger or can be read on demand. |
| commands/prompts/workflows | `.cursor/commands/`, `.github/prompts/`, `.windsurf/workflows/` | Entry points explicitly invoked by the user. |
## Three Platform Integration Modes
### 1. Hook / Extension Driven
These platforms can trigger scripts or plugins on specific events and actively inject Trellis context into AI.
Common capabilities:
- session-start injection of a `.trellis/` overview.
- workflow-state hints for each user turn.
- PRD/spec/research injection when sub-agents start.
- Shell commands inheriting session identity.
To change "when the AI knows what," inspect hooks/plugins/extensions and settings first.
### 2. Agent Prelude / Pull-Based
Some platforms cannot reliably let hooks rewrite sub-agent prompts, so the agent file itself instructs the agent to read the active task, PRD, and JSONL context after startup.
To change how sub-agents load context, inspect the agent files themselves.
### 3. Main-Session Workflow
Some platforms do not have Trellis sub-agent or hook capabilities. They rely on workflows/skills/commands to guide the main-session AI to read files, run scripts, and move tasks forward.
To change behavior, inspect platform workflows/skills/commands and `.trellis/workflow.md`.
## Local Modification Order
When the user asks to customize behavior for a platform, the AI should inspect files in this order:
1. Read `.trellis/workflow.md` to confirm the shared flow.
2. Read the target platform's settings/config to see which hooks/agents/skills/commands are registered.
3. Read the target platform's agents/skills/commands/hooks.
4. Modify the local file closest to the user's need.
5. If the change affects the shared flow, synchronize `.trellis/workflow.md` or `.trellis/spec/`.
Do not modify only platform files and forget the shared workflow. Do not modify only `.trellis/workflow.md` and forget that platform entry points may still contain old descriptions.

View File

@@ -0,0 +1,74 @@
# Platform File Map
This page lists common Trellis file locations in a user project by platform. Whether a platform directory exists in an actual project depends on which `trellis init --<platform>` commands the user ran.
## Matrix
| Platform | CLI flag | Main directory | Skill directory | Agent directory | Hooks/extensions |
| --- | --- | --- | --- | --- | --- |
| Claude Code | `--claude` | `.claude/` | `.claude/skills/` | `.claude/agents/` | `.claude/hooks/` + `.claude/settings.json` |
| Cursor | `--cursor` | `.cursor/` | `.cursor/skills/` | `.cursor/agents/` | `.cursor/hooks.json` + `.cursor/hooks/` |
| OpenCode | `--opencode` | `.opencode/` | `.opencode/skills/` | `.opencode/agents/` | `.opencode/plugins/` |
| Codex | `--codex` | `.codex/` | `.agents/skills/` | `.codex/agents/` | `.codex/hooks/` + `.codex/hooks.json` |
| Kilo | `--kilo` | `.kilocode/` | `.kilocode/skills/` | Usually none | `.kilocode/workflows/` |
| Kiro | `--kiro` | `.kiro/` | `.kiro/skills/` | `.kiro/agents/` | `.kiro/hooks/` |
| Gemini CLI | `--gemini` | `.gemini/` | `.agents/skills/` | `.gemini/agents/` | `.gemini/settings.json` + `.gemini/hooks/` |
| Antigravity | `--antigravity` | `.agent/` | `.agent/skills/` | Usually none | `.agent/workflows/` |
| Windsurf | `--windsurf` | `.windsurf/` | `.windsurf/skills/` | Usually none | `.windsurf/workflows/` |
| Qoder | `--qoder` | `.qoder/` | `.qoder/skills/` | `.qoder/agents/` | `.qoder/hooks/` + `.qoder/settings.json` |
| CodeBuddy | `--codebuddy` | `.codebuddy/` | `.codebuddy/skills/` | `.codebuddy/agents/` | `.codebuddy/hooks/` + `.codebuddy/settings.json` |
| GitHub Copilot | `--copilot` | `.github/` | `.github/skills/` | `.github/agents/` | `.github/copilot/hooks/` + prompts |
| Factory Droid | `--droid` | `.factory/` | `.factory/skills/` | `.factory/droids/` | `.factory/hooks/` + settings |
| Pi Agent | `--pi` | `.pi/` | `.pi/skills/` | `.pi/agents/` | `.pi/extensions/trellis/` + `.pi/settings.json` |
## Capability Groups
### Trellis Sub-Agent Support
These platforms usually have `trellis-research`, `trellis-implement`, and `trellis-check` files:
- Claude Code
- Cursor
- OpenCode
- Codex
- Kiro
- Gemini CLI
- Qoder
- CodeBuddy
- GitHub Copilot
- Factory Droid
- Pi Agent
When changing implementation/check/research behavior, look for the corresponding platform agent files first.
### Main-Session Workflow Platforms
These platforms rely more on workflows/skills to guide the main session:
- Kilo
- Antigravity
- Windsurf
When changing behavior, inspect workflows and skills first. Do not assume Trellis sub-agents exist.
### Shared `.agents/skills/`
Codex writes the shared `.agents/skills/` layer. Some tools that support agentskills.io can also read this directory. If the user wants multiple compatible tools to share one skill, consider `.agents/skills/` first, but do not assume every platform reads it.
## Decision Rules When Modifying Platform Files
1. User specified a platform: modify only that platform directory unless shared workflow/spec files must also change.
2. User says "all platforms should do this": synchronize equivalent entry points platform by platform; do not modify only one directory.
3. User only says "my AI": inspect the configuration directories that actually exist in the project and infer the current AI platform.
4. User wants project rules: prefer `.trellis/spec/` or a project-local skill.
5. User wants Trellis behavior: edit `.trellis/workflow.md` plus platform hooks/agents/skills/commands.
## When Paths Differ
Platform ecosystems change, and user projects may already be customized. If this table disagrees with local files, use the actual settings/config in the user project as authoritative:
- Check the hook that settings registers.
- Check the script that a command/prompt/workflow points to.
- Judge behavior by the read rules currently written in the agent file.
Do not delete a custom file just because it is not listed in this path table.

View File

@@ -0,0 +1,83 @@
# Skills, Commands, Prompts, And Workflows
Skills and commands are textual entry points for user interaction with Trellis. Different platforms use different names, but their core purpose is the same: tell the AI how to enter the Trellis flow when the user expresses a certain intent.
## Conceptual Differences
| Type | Trigger mode | Best for |
| --- | --- | --- |
| skill | AI auto-match or explicit user mention | Long-term capabilities, workflow rules, modification guides. |
| command | Explicit user invocation | Clear operation entry points such as continue and finish-work. |
| prompt | Explicit user invocation or platform selection | Similar to command, but in a platform prompt format. |
| workflow | Explicit user selection or platform auto-match | Guides the main session when no sub-agent/hook exists. |
Trellis workflow skills usually share one semantic set: brainstorm, before-dev, check, update-spec, break-loop. Multi-file built-in skills such as `trellis-meta` use layered references.
## Common Paths
| Platform | Common entries |
| --- | --- |
| Claude Code | `.claude/skills/`, `.claude/commands/` |
| Cursor | `.cursor/skills/`, `.cursor/commands/` |
| OpenCode | `.opencode/skills/`, `.opencode/commands/` |
| Codex | `.agents/skills/`, `.codex/skills/` |
| Kilo | `.kilocode/skills/`, `.kilocode/workflows/` |
| Kiro | `.kiro/skills/` |
| Gemini CLI | `.agents/skills/`, `.gemini/commands/` |
| Antigravity | `.agent/skills/`, `.agent/workflows/` |
| Windsurf | `.windsurf/skills/`, `.windsurf/workflows/` |
| Qoder | `.qoder/skills/`, `.qoder/commands/` |
| CodeBuddy | `.codebuddy/skills/`, `.codebuddy/commands/` |
| GitHub Copilot | `.github/skills/`, `.github/prompts/` |
| Factory Droid | `.factory/skills/`, `.factory/commands/` |
| Pi Agent | `.pi/skills/` |
In a user project, use the files actually generated by init as authoritative.
## Skill Structure
A common skill is a directory:
```text
trellis-meta/
├── SKILL.md
└── references/
```
`SKILL.md` should tell the AI:
- When to use this skill.
- Which reference to read first for the current task.
- What not to do.
References hold longer explanations so the entry file does not contain everything.
## Command/Prompt/Workflow Structure
Commands, prompts, and workflows are usually single files. Their content should include:
- When to use it.
- Which `.trellis/` files to read.
- Which scripts to run.
- How to report after completion.
They should not store task state; task state belongs in `.trellis/tasks/` and `.trellis/.runtime/`.
## Local Change Scenarios
| User need | Edit location |
| --- | --- |
| Change AI auto-trigger rules | The corresponding skill's frontmatter description. |
| Change user command behavior | The corresponding command/prompt/workflow file. |
| Add a project-local skill | Platform skill directory, or shared `.agents/skills/`. |
| Let multiple platforms share one capability | Write equivalent skills in each platform skill directory, or use the `.agents/skills/` shared layer on platforms that support it. |
| Change finish/continue entry points | Platform commands/prompts/workflows. |
## Modification Principles
1. **Keep entry files short; references carry long content**. This matters especially for multi-file skills like `trellis-meta`.
2. **Make trigger descriptions specific**. A description that is too broad can mis-trigger; one that is too narrow may not trigger.
3. **Keep the same semantics consistent across platforms**. File formats can differ, but behavior descriptions should match.
4. **Put project-specific capabilities in local skills**. Do not put team-private flows into public `trellis-meta`.
If the user only wants local AI to know one more project rule, usually create a project-local skill or update `.trellis/spec/` instead of changing a Trellis built-in workflow skill.

View File

@@ -0,0 +1,356 @@
---
name: trellis-update-spec
description: "Captures executable contracts and coding conventions into .trellis/spec/ documents. Use when learning something valuable from debugging, implementing, or discussion that should be preserved for future sessions."
---
# Update Code-Spec - Capture Executable Contracts
When you learn something valuable (from debugging, implementing, or discussion), use this to update the relevant code-spec documents.
**Timing**: After completing a task, fixing a bug, or discovering a new pattern
---
## Code-Spec First Rule (CRITICAL)
In this project, "spec" for implementation work means **code-spec**:
- Executable contracts (not principle-only text)
- Concrete signatures, payload fields, env keys, and boundary behavior
- Testable validation/error behavior
If the change touches infra or cross-layer contracts, code-spec depth is mandatory.
### Mandatory Triggers
Apply code-spec depth when the change includes any of:
- New/changed command or API signature
- Cross-layer request/response contract change
- Database schema/migration change
- Infra integration (storage, queue, cache, secrets, env wiring)
### Mandatory Output (7 Sections)
For triggered tasks, include all sections below:
1. Scope / Trigger
2. Signatures (command/API/DB)
3. Contracts (request/response/env)
4. Validation & Error Matrix
5. Good/Base/Bad Cases
6. Tests Required (with assertion points)
7. Wrong vs Correct (at least one pair)
---
## When to Update Code-Specs
| Trigger | Example | Target Spec |
|---------|---------|-------------|
| **Implemented a feature** | Added a new integration or module | Relevant spec file |
| **Made a design decision** | Chose extensibility pattern over simplicity | Relevant spec + "Design Decisions" section |
| **Fixed a bug** | Found a subtle issue with error handling | Relevant spec (e.g., error-handling docs) |
| **Discovered a pattern** | Found a better way to structure code | Relevant spec file |
| **Hit a gotcha** | Learned that X must be done before Y | Relevant spec + "Common Mistakes" section |
| **Established a convention** | Team agreed on naming pattern | Quality guidelines |
| **New thinking trigger** | "Don't forget to check X before doing Y" | `guides/*.md` (as a checklist item) |
**Key Insight**: Code-spec updates are NOT just for problems. Every feature implementation contains design decisions and contracts that future AI/developers need to execute safely.
---
## Spec Structure Overview
```
.trellis/spec/
├── <layer>/ # Per-layer coding standards (e.g., backend/, frontend/, api/)
│ ├── index.md # Overview and links
│ └── *.md # Topic-specific guidelines
└── guides/ # Thinking checklists (NOT coding specs!)
├── index.md # Guide index
└── *.md # Topic-specific guides
```
### CRITICAL: Code-Spec vs Guide - Know the Difference
| Type | Location | Purpose | Content Style |
|------|----------|---------|---------------|
| **Code-Spec** | `<layer>/*.md` | Tell AI "how to implement safely" | Signatures, contracts, matrices, cases, test points |
| **Guide** | `guides/*.md` | Help AI "what to think about" | Checklists, questions, pointers to specs |
**Decision Rule**: Ask yourself:
- "This is **how to write** the code" → Put in a spec layer directory
- "This is **what to consider** before writing" → Put in `guides/`
**Example**:
| Learning | Wrong Location | Correct Location |
|----------|----------------|------------------|
| "Use API X not API Y for this task" | ❌ `guides/` (too specific for a thinking guide) | ✅ Relevant spec file (concrete convention) |
| "Remember to check X when doing Y" | ❌ Spec file (too abstract for a spec) | ✅ `guides/` (thinking checklist) |
**Guides should be short checklists that point to specs**, not duplicate the detailed rules.
---
## Update Process
### Step 1: Identify What You Learned
Answer these questions:
1. **What did you learn?** (Be specific)
2. **Why is it important?** (What problem does it prevent?)
3. **Where does it belong?** (Which spec file?)
### Step 2: Classify the Update Type
| Type | Description | Action |
|------|-------------|--------|
| **Design Decision** | Why we chose approach X over Y | Add to "Design Decisions" section |
| **Project Convention** | How we do X in this project | Add to relevant section with examples |
| **New Pattern** | A reusable approach discovered | Add to "Patterns" section |
| **Forbidden Pattern** | Something that causes problems | Add to "Anti-patterns" or "Don't" section |
| **Common Mistake** | Easy-to-make error | Add to "Common Mistakes" section |
| **Convention** | Agreed-upon standard | Add to relevant section |
| **Gotcha** | Non-obvious behavior | Add warning callout |
### Step 3: Read the Target Code-Spec
Before editing, read the current code-spec to:
- Understand existing structure
- Avoid duplicating content
- Find the right section for your update
```bash
cat .trellis/spec/<category>/<file>.md
```
### Step 4: Make the Update
Follow these principles:
1. **Be Specific**: Include concrete examples, not just abstract rules
2. **Explain Why**: State the problem this prevents
3. **Show Contracts**: Add signatures, payload fields, and error behavior
4. **Show Code**: Add code snippets for key patterns
5. **Keep it Short**: One concept per section
### Step 5: Update the Index (if needed)
If you added a new section or the code-spec status changed, update the category's `index.md`.
---
## Update Templates
### Mandatory Template for Infra/Cross-Layer Work
```markdown
## Scenario: <name>
### 1. Scope / Trigger
- Trigger: <why this requires code-spec depth>
### 2. Signatures
- Backend command/API/DB signature(s)
### 3. Contracts
- Request fields (name, type, constraints)
- Response fields (name, type, constraints)
- Environment keys (required/optional)
### 4. Validation & Error Matrix
- <condition> -> <error>
### 5. Good/Base/Bad Cases
- Good: ...
- Base: ...
- Bad: ...
### 6. Tests Required
- Unit/Integration/E2E with assertion points
### 7. Wrong vs Correct
#### Wrong
...
#### Correct
...
```
### Adding a Design Decision
```markdown
### Design Decision: [Decision Name]
**Context**: What problem were we solving?
**Options Considered**:
1. Option A - brief description
2. Option B - brief description
**Decision**: We chose Option X because...
**Example**:
\`\`\`typescript
// How it's implemented
code example
\`\`\`
**Extensibility**: How to extend this in the future...
```
### Adding a Project Convention
```markdown
### Convention: [Convention Name]
**What**: Brief description of the convention.
**Why**: Why we do it this way in this project.
**Example**:
\`\`\`typescript
// How to follow this convention
code example
\`\`\`
**Related**: Links to related conventions or specs.
```
### Adding a New Pattern
```markdown
### Pattern Name
**Problem**: What problem does this solve?
**Solution**: Brief description of the approach.
**Example**:
\`\`\`
// Good
code example
// Bad
code example
\`\`\`
**Why**: Explanation of why this works better.
```
### Adding a Forbidden Pattern
```markdown
### Don't: Pattern Name
**Problem**:
\`\`\`
// Don't do this
bad code example
\`\`\`
**Why it's bad**: Explanation of the issue.
**Instead**:
\`\`\`
// Do this instead
good code example
\`\`\`
```
### Adding a Common Mistake
```markdown
### Common Mistake: Description
**Symptom**: What goes wrong
**Cause**: Why this happens
**Fix**: How to correct it
**Prevention**: How to avoid it in the future
```
### Adding a Gotcha
```markdown
> **Warning**: Brief description of the non-obvious behavior.
>
> Details about when this happens and how to handle it.
```
---
## Interactive Mode
If you're unsure what to update, answer these prompts:
1. **What did you just finish?**
- [ ] Fixed a bug
- [ ] Implemented a feature
- [ ] Refactored code
- [ ] Had a discussion about approach
2. **What did you learn or decide?**
- Design decision (why X over Y)
- Project convention (how we do X)
- Non-obvious behavior (gotcha)
- Better approach (pattern)
3. **Would future AI/developers need to know this?**
- To understand how the code works → Yes, update spec
- To maintain or extend the feature → Yes, update spec
- To avoid repeating mistakes → Yes, update spec
- Purely one-off implementation detail → Maybe skip
4. **Which area does it relate to?**
- [ ] Backend code
- [ ] Frontend code
- [ ] Cross-layer data flow
- [ ] Code organization/reuse
- [ ] Quality/testing
---
## Quality Checklist
Before finishing your code-spec update:
- [ ] Is the content specific and actionable?
- [ ] Did you include a code example?
- [ ] Did you explain WHY, not just WHAT?
- [ ] Did you include executable signatures/contracts?
- [ ] Did you include validation and error matrix?
- [ ] Did you include Good/Base/Bad cases?
- [ ] Did you include required tests with assertion points?
- [ ] Is it in the right code-spec file?
- [ ] Does it duplicate existing content?
- [ ] Would a new team member understand it?
---
## Relationship to Other Commands
```
Development Flow:
Learn something → /trellis:update-spec → Knowledge captured
↑ ↓
/trellis:break-loop ←──────────────────── Future sessions benefit
(deep bug analysis)
```
- `/trellis:break-loop` - Analyzes bugs deeply, often reveals spec updates needed
- `/trellis:update-spec` - Actually makes the updates
- `/trellis:finish-work` - Reminds you to check if specs need updates
---
## Core Philosophy
> **Code-specs are living documents. Every debugging session, every "aha moment" is an opportunity to make the implementation contract clearer.**
The goal is **institutional memory**:
- What one person learns, everyone benefits from
- What AI learns in one session, persists to future sessions
- Mistakes become documented guardrails

View File

@@ -0,0 +1,108 @@
---
name: trellis-check
description: Trellis quality check agent. Use this exact agent for Trellis task verification, check.jsonl context injection, and self-fixing code review. Do not use generic/default/generalPurpose agents for Trellis checks.
tools: Read, Write, Edit, Bash, Glob, Grep, mcp__exa__web_search_exa, mcp__exa__get_code_context_exa
---
# Check Agent
You are the Check Agent in the Trellis workflow.
## Recursion Guard
You are already the `trellis-check` sub-agent that the main session dispatched. Do the review and fixes directly.
- Do NOT spawn another `trellis-check` or `trellis-implement` sub-agent.
- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role.
- Only the main session may dispatch Trellis implement/check agents. If more implementation work is needed, report that recommendation instead of spawning.
## Trellis Context Loading Protocol
Look for the `<!-- trellis-hook-injected -->` marker in your input above.
- **If the marker is present**: prd / spec / research files have already been auto-loaded for you above. Proceed with the check work directly.
- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: <path>`, then Read `<task-path>/prd.md` and the spec files listed in `<task-path>/check.jsonl` yourself before doing the work.
## Context
Before checking, read:
- `.trellis/spec/` - Development guidelines
- Pre-commit checklist for quality standards
## Core Responsibilities
1. **Get code changes** - Use git diff to get uncommitted code
2. **Check against specs** - Verify code follows guidelines
3. **Self-fix** - Fix issues yourself, not just report them
4. **Run verification** - typecheck and lint
## Important
**Fix issues yourself**, don't just report them.
You have write and edit tools, you can modify code directly.
---
## Workflow
### Step 1: Get Changes
```bash
git diff --name-only # List changed files
git diff # View specific changes
```
### Step 2: Check Against Specs
Read relevant specs in `.trellis/spec/` to check code:
- Does it follow directory structure conventions
- Does it follow naming conventions
- Does it follow code patterns
- Are there missing types
- Are there potential bugs
### Step 3: Self-Fix
After finding issues:
1. Fix the issue directly (use edit tool)
2. Record what was fixed
3. Continue checking other issues
### Step 4: Run Verification
Run project's lint and typecheck commands to verify changes.
If failed, fix issues and re-run.
---
## Report Format
```markdown
## Self-Check Complete
### Files Checked
- src/components/Feature.tsx
- src/hooks/useFeature.ts
### Issues Found and Fixed
1. `<file>:<line>` - <what was fixed>
2. `<file>:<line>` - <what was fixed>
### Issues Not Fixed
(If there are issues that cannot be self-fixed, list them here with reasons)
### Verification Results
- TypeCheck: Passed
- Lint: Passed
### Summary
Checked X files, found Y issues, all fixed.
```

View File

@@ -0,0 +1,108 @@
---
name: trellis-implement
description: Trellis implementation agent. Use this exact agent for Trellis task implementation, implement.jsonl context injection, and hook-injection tests. Do not use generic/default/generalPurpose agents for Trellis implementation. No git commit allowed.
tools: Read, Write, Edit, Bash, Glob, Grep, mcp__exa__web_search_exa, mcp__exa__get_code_context_exa
---
# Implement Agent
You are the Implement Agent in the Trellis workflow.
## Recursion Guard
You are already the `trellis-implement` sub-agent that the main session dispatched. Do the implementation work directly.
- Do NOT spawn another `trellis-implement` or `trellis-check` sub-agent.
- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role.
- Only the main session may dispatch Trellis implement/check agents. If more parallel work is needed, report that recommendation instead of spawning.
## Trellis Context Loading Protocol
Look for the `<!-- trellis-hook-injected -->` marker in your input above.
- **If the marker is present**: prd / spec / research files have already been auto-loaded for you above. Proceed with the implementation work directly.
- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: <path>`, then Read `<task-path>/prd.md`, `<task-path>/info.md` (if it exists), and the spec files listed in `<task-path>/implement.jsonl` yourself before doing the work.
## Context
Before implementing, read:
- `.trellis/workflow.md` - Project workflow
- `.trellis/spec/` - Development guidelines
- Task `prd.md` - Requirements document
- Task `info.md` - Technical design (if exists)
## Core Responsibilities
1. **Understand specs** - Read relevant spec files in `.trellis/spec/`
2. **Understand requirements** - Read prd.md and info.md
3. **Implement features** - Write code following specs and design
4. **Self-check** - Ensure code quality
5. **Report results** - Report completion status
## Forbidden Operations
**Do NOT execute these git commands:**
- `git commit`
- `git push`
- `git merge`
---
## Workflow
### 1. Understand Specs
Read relevant specs based on task type:
- Spec layers: `.trellis/spec/<package>/<layer>/`
- Shared guides: `.trellis/spec/guides/`
### 2. Understand Requirements
Read the task's prd.md and info.md:
- What are the core requirements
- Key points of technical design
- Which files to modify/create
### 3. Implement Features
- Write code following specs and technical design
- Follow existing code patterns
- Only do what's required, no over-engineering
### 4. Verify
Run project's lint and typecheck commands to verify changes.
---
## Report Format
```markdown
## Implementation Complete
### Files Modified
- `src/components/Feature.tsx` - New component
- `src/hooks/useFeature.ts` - New hook
### Implementation Summary
1. Created Feature component...
2. Added useFeature hook...
### Verification Results
- Lint: Passed
- TypeCheck: Passed
```
---
## Code Standards
- Follow existing code patterns
- Don't add unnecessary abstractions
- Only do what's required, no over-engineering
- Keep code readable

View File

@@ -0,0 +1,136 @@
---
name: trellis-research
description: Trellis research agent. Use this exact agent for Trellis task research and research/ persistence. Do not use generic/default/generalPurpose agents for Trellis research.
tools: Read, Write, Glob, Grep, Bash, mcp__exa__web_search_exa, mcp__exa__get_code_context_exa, Skill, mcp__chrome-devtools__*
---
# Research Agent
You are the Research Agent in the Trellis workflow.
## Core Principle
**You do one thing: find, explain, and PERSIST information.**
Conversations get compacted; files don't. Every research output MUST end up as a file under `{TASK_DIR}/research/`. Returning findings only through the chat reply is a failure — the caller cannot read them next session.
---
## Core Responsibilities
1. **Internal Search** — locate files/components, understand code logic, discover patterns (Glob, Grep, Read)
2. **External Search** — library docs, API references, best practices (web search)
3. **Persist** — write each research topic to `{TASK_DIR}/research/<topic>.md`
4. **Report** — return file paths + one-line summaries to the main agent (not full content)
---
## Workflow
### Step 1: Resolve Current Task
Run `python ./.trellis/scripts/task.py current --source` → active task path. If no active task is set, ask the user where to write output; do NOT guess.
Ensure `{TASK_DIR}/research/` exists:
```bash
mkdir -p <TASK_DIR>/research
```
### Step 2: Understand Search Request
Classify: internal / external / mixed. Determine scope (global / specific directory) and expected shape (file list / pattern notes / tech comparison).
### Step 3: Execute Search
Run independent searches in parallel (Glob + Grep + web) for efficiency.
### Step 4: Persist Each Topic
For each distinct research topic, Write a markdown file at `{TASK_DIR}/research/<topic-slug>.md`. Use the File Format below.
### Step 5: Report to Main Agent
Reply with ONLY:
- List of files written (paths relative to repo root)
- One-line summary per file
- Any critical caveats that the main agent needs to know right now
Do NOT paste full research content into the reply. The files are the contract.
---
## Scope Limits (Strict)
### Write ALLOWED
- `{TASK_DIR}/research/*.md` — your own output
- Creating `{TASK_DIR}/research/` if it doesn't exist (via `mkdir -p`)
### Write FORBIDDEN
- Code files (`src/`, `lib/`, …)
- Spec files (`.trellis/spec/`) — main agent should use `update-spec` skill instead
- `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.claude/`, `.cursor/`, etc.)
- Other task directories
- Any git operation (commit / push / branch / merge)
If the user asks you to edit code, decline and suggest spawning `implement` instead.
---
## File Format
Each `{TASK_DIR}/research/<topic>.md` should follow:
```markdown
# Research: <topic>
- **Query**: <original query>
- **Scope**: <internal / external / mixed>
- **Date**: <YYYY-MM-DD>
## Findings
### Files Found
| File Path | Description |
|---|---|
| `src/services/xxx.ts` | Main implementation |
| `src/types/xxx.ts` | Type definitions |
### Code Patterns
<describe patterns, cite file:line>
### External References
- [Library X docs](url) — <why relevant, version constraints>
### Related Specs
- `.trellis/spec/xxx.md` — <description>
## Caveats / Not Found
<anything incomplete or uncertain>
```
---
## Guidelines
### DO
- Provide specific file paths and line numbers
- Quote actual code snippets
- Persist every topic to its own file
- Return file paths in your reply, not the full content
- Mark "not found" explicitly when searches come up empty
### DON'T
- Don't write code or modify files outside `{TASK_DIR}/research/`
- Don't guess uncertain info
- Don't paste full research text into the reply (files are the deliverable)
- Don't propose improvements or critique implementation (that's not your role)

View File

@@ -0,0 +1,55 @@
# Continue Current Task
Resume work on the current task — pick up at the right phase/step in `.trellis/workflow.md`.
---
## Step 1: Load Current Context
```bash
python ./.trellis/scripts/get_context.py
```
Confirms: current task, git state, recent commits.
## Step 2: Load the Phase Index
```bash
python ./.trellis/scripts/get_context.py --mode phase
```
Shows the Phase Index (Plan / Execute / Finish) with routing + skill mapping.
## Step 3: Decide Where You Are
`get_context.py` shows the active task's `status` field. Route by `status` + artifact presence:
- `status=planning` + no `prd.md`**1.1** (load `trellis-brainstorm`)
- `status=planning` + `prd.md` exists + `implement.jsonl` not curated (only the seed `_example` row) → **1.3**
- `status=planning` + `prd.md` + curated `implement.jsonl`**1.4** (run `task.py start` to enter Phase 2)
- `status=in_progress` + implementation not started → **2.1**
- `status=in_progress` + implementation done, not yet checked → **2.2**
- `status=in_progress` + check passed → **3.1**
- `status=completed` (rare; usually archived immediately) → archive flow
Phase rules (full detail in `.trellis/workflow.md`):
1. Run steps **in order** within a phase — `[required]` steps must not be skipped
2. `[once]` steps are already done if the output exists (e.g., `prd.md` for 1.1; `implement.jsonl` with curated entries for 1.3) — skip them
3. You may go back to an earlier phase if discoveries require it
## Step 4: Load the Specific Step
Once you know which step to resume at:
```bash
python ./.trellis/scripts/get_context.py --mode phase --step <X.X> --platform cursor
```
Follow the loaded instructions. After each `[required]` step completes, move to the next.
---
## Reference
Full workflow, skill routing table, and the DO-NOT-skip table live in `.trellis/workflow.md`. This command is only an entry point — the canonical guidance is there.

View File

@@ -0,0 +1,66 @@
# Finish Work
Wrap up the current session: archive the active task (and any other completed-but-unarchived tasks the user wants to clean up) and record the session journal. Code commits are NOT done here — those happen in workflow Phase 3.4 before you invoke this command.
## Step 1: Survey current state
```bash
python ./.trellis/scripts/get_context.py --mode record
```
This prints:
- **My active tasks** — review whether any besides the current one are actually done (code merged, AC met) and should be archived this round.
- **Git status** — quick visual on what's dirty.
- **Recent commits** — you'll need their hashes in Step 4 for `--commit`.
If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless.
## Step 2: Sanity check — classify dirty paths
Run:
```bash
git status --porcelain
```
Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this skill's own work.
For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics:
- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task
- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task
- Paths in unrelated areas you have no recollection of touching this session → other parallel work
Then route:
- **Any remaining path looks like current-task work** — bail out with:
> "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running `/trellis-finish-work`."
Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there.
- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3:
> "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`."
- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer.
## Step 3: Archive task(s)
```bash
python ./.trellis/scripts/task.py archive <task-name>
```
At minimum: the current active task (if any). Plus any extra tasks the user confirmed in Step 1. Each archive produces a `chore(task): archive ...` commit via the script's auto-commit.
If there is no active task and the user did not confirm any cleanup archives, skip this step.
## Step 4: Record session journal
```bash
python ./.trellis/scripts/add_session.py \
--title "Session Title" \
--commit "hash1,hash2" \
--summary "Brief summary"
```
Use the work-commit hashes produced in Phase 3.4 (visible in Step 1's `Recent commits` list, or via `git log --oneline`) for `--commit`. Do not include the archive commit hashes from Step 3. This produces a `chore: record journal` commit.
Final git log order: `<work commits from 3.4>``chore(task): archive ...` (one or more) → `chore: record journal`.

30
.cursor/hooks.json Normal file
View File

@@ -0,0 +1,30 @@
{
"version": 1,
"hooks": {
"preToolUse": [
{
"command": "python .cursor/hooks/inject-subagent-context.py",
"matcher": "Task|Subagent",
"timeout": 30
}
],
"sessionStart": [
{
"command": "python .cursor/hooks/session-start.py",
"timeout": 10
}
],
"beforeSubmitPrompt": [
{
"command": "python .cursor/hooks/inject-workflow-state.py",
"timeout": 5
}
],
"beforeShellExecution": [
{
"command": "python .cursor/hooks/inject-shell-session-context.py",
"timeout": 5
}
]
}
}

View File

@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Cursor beforeShellExecution hook: bridge conversation identity to task.py.
Cursor's shell command environment does not inherit SessionStart data. This
hook writes a short-lived runtime ticket before Cursor runs a shell command
that calls `task.py start/current/finish`. The task script then consumes the
ticket only when it has no native session environment.
"""
from __future__ import annotations
import hashlib
import json
import os
import shlex
import sys
import time
from pathlib import Path
from typing import Any
DIR_WORKFLOW = ".trellis"
DIR_RUNTIME = ".runtime"
DIR_CURSOR_SHELL = "cursor-shell"
SESSION_SUBCOMMANDS = {"start", "current", "finish"}
TICKET_TTL_SECONDS = 30
CONTEXT_IDENTITY_KEYS = (
"session_id",
"sessionId",
"sessionID",
"conversation_id",
"conversationId",
"conversationID",
"transcript_path",
"transcriptPath",
"transcript",
)
def _string_value(value: Any) -> str | None:
if isinstance(value, str):
stripped = value.strip()
return stripped or None
return None
def _find_trellis_root(start: Path) -> Path | None:
current = start.resolve()
while True:
if (current / DIR_WORKFLOW).is_dir():
return current
if current == current.parent:
return None
current = current.parent
def _runtime_ticket_dir(root: Path) -> Path:
return root / DIR_WORKFLOW / DIR_RUNTIME / DIR_CURSOR_SHELL
def _load_active_task_resolver(root: Path):
scripts_dir = root / DIR_WORKFLOW / "scripts"
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
from common.active_task import resolve_context_key # type: ignore[import-not-found]
return resolve_context_key
def _extract_task_subcommands(command: str) -> list[dict[str, str]]:
try:
tokens = shlex.split(command, posix=os.name != "nt")
except ValueError:
return []
subcommands: list[dict[str, str]] = []
for index, token in enumerate(tokens[:-1]):
if Path(token.strip("\"'")).name != "task.py":
continue
name = tokens[index + 1]
if name not in SESSION_SUBCOMMANDS:
continue
item = {"name": name}
if name == "start" and index + 2 < len(tokens):
item["task_ref"] = tokens[index + 2]
subcommands.append(item)
return subcommands
def _cleanup_expired_tickets(ticket_dir: Path, now: float) -> None:
if not ticket_dir.is_dir():
return
for ticket_path in ticket_dir.glob("*.json"):
try:
data = json.loads(ticket_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
continue
expires_at = data.get("expires_at_epoch")
if isinstance(expires_at, (int, float)) and expires_at < now:
try:
ticket_path.unlink()
except OSError:
pass
def _has_context_identity(hook_input: dict[str, Any]) -> bool:
return any(_string_value(hook_input.get(key)) for key in CONTEXT_IDENTITY_KEYS)
def _write_ticket(
root: Path,
hook_input: dict[str, Any],
context_key: str,
subcommands: list[dict[str, str]],
) -> None:
now = time.time()
ticket_dir = _runtime_ticket_dir(root)
ticket_dir.mkdir(parents=True, exist_ok=True)
_cleanup_expired_tickets(ticket_dir, now)
command = _string_value(hook_input.get("command")) or ""
digest = hashlib.sha256(
f"{context_key}\0{command}\0{now}".encode("utf-8"),
).hexdigest()[:16]
ticket_path = ticket_dir / f"{int(now * 1000)}-{digest}.json"
payload = {
"platform": "cursor",
"context_key": context_key,
"conversation_id": _string_value(hook_input.get("conversation_id")),
"session_id": _string_value(hook_input.get("session_id")),
"generation_id": _string_value(hook_input.get("generation_id")),
"cwd": _string_value(hook_input.get("cwd")),
"command": command,
"subcommands": subcommands,
"created_at_epoch": now,
"expires_at_epoch": now + TICKET_TTL_SECONDS,
}
ticket_path.write_text(
json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
def main() -> int:
if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
return 0
try:
hook_input = json.loads(sys.stdin.read())
except (json.JSONDecodeError, ValueError):
hook_input = {}
if not isinstance(hook_input, dict):
hook_input = {}
command = _string_value(hook_input.get("command")) or ""
subcommands = _extract_task_subcommands(command)
if not subcommands:
return 0
cwd = Path(_string_value(hook_input.get("cwd")) or os.getcwd())
root = _find_trellis_root(cwd)
if root is None:
return 0
if not _has_context_identity(hook_input):
return 0
resolve_context_key = _load_active_task_resolver(root)
context_key = resolve_context_key(hook_input, platform="cursor")
if not context_key:
return 0
try:
_write_ticket(root, hook_input, context_key, subcommands)
except OSError:
return 0
print(json.dumps({"permission": "allow"}, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,749 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Multi-Platform Sub-Agent Context Injection Hook
Injects task-specific context when sub-agents (implement, check, research) are spawned.
Core Design Philosophy:
- Hook is responsible for injecting all context, subagent works autonomously with complete info
- Each agent has a dedicated jsonl file defining its context
- No resume needed, no segmentation, behavior controlled by code not prompt
Trigger: PreToolUse (before Task tool call)
Context Source: Trellis active task resolver points to task directory
- implement.jsonl - Implement agent dedicated context
- check.jsonl - Check agent dedicated context
- prd.md - Requirements document
- info.md - Technical design
- codex-review-output.txt - Code Review results
"""
from __future__ import annotations
# IMPORTANT: Suppress all warnings FIRST
import warnings
warnings.filterwarnings("ignore")
import json
import os
import sys
from pathlib import Path
from typing import Any
# IMPORTANT: Force stdout to use UTF-8 on Windows
# This fixes UnicodeEncodeError when outputting non-ASCII characters
if sys.platform.startswith("win"):
import io as _io
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
elif hasattr(sys.stdout, "detach"):
sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr]
# =============================================================================
# Path Constants (change here to rename directories)
# =============================================================================
DIR_WORKFLOW = ".trellis"
DIR_SPEC = "spec"
FILE_TASK_JSON = "task.json"
# =============================================================================
# Subagent Constants (change here to rename subagent types)
# =============================================================================
AGENT_IMPLEMENT = "trellis-implement"
AGENT_CHECK = "trellis-check"
AGENT_RESEARCH = "trellis-research"
# Agents that require a task directory
AGENTS_REQUIRE_TASK = (AGENT_IMPLEMENT, AGENT_CHECK)
# All supported agents
AGENTS_ALL = (AGENT_IMPLEMENT, AGENT_CHECK, AGENT_RESEARCH)
def find_repo_root(start_path: str) -> str | None:
"""
Find git repo root from start_path upwards
Returns:
Repo root path, or None if not found
"""
current = Path(start_path).resolve()
while current != current.parent:
if (current / ".git").exists():
return str(current)
current = current.parent
return None
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 ".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 get_current_task(repo_root: str, input_data: dict) -> str | None:
"""Resolve current task directory through the unified active task resolver."""
scripts_dir = Path(repo_root) / DIR_WORKFLOW / "scripts"
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
try:
from common.active_task import resolve_active_task # type: ignore[import-not-found]
except Exception:
return None
active = resolve_active_task(
Path(repo_root),
input_data,
platform=_detect_platform(input_data),
)
return active.task_path
def read_file_content(base_path: str, file_path: str) -> str | None:
"""Read file content, return None if file doesn't exist"""
full_path = os.path.join(base_path, file_path)
if os.path.exists(full_path) and os.path.isfile(full_path):
try:
with open(full_path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return None
return None
def read_directory_contents(
base_path: str, dir_path: str, max_files: int = 20
) -> list[tuple[str, str]]:
"""
Read all .md files in a directory
Args:
base_path: Base path (usually repo_root)
dir_path: Directory relative path
max_files: Max files to read (prevent huge directories)
Returns:
[(file_path, content), ...]
"""
full_path = os.path.join(base_path, dir_path)
if not os.path.exists(full_path) or not os.path.isdir(full_path):
return []
results = []
try:
# Only read .md files, sorted by filename
md_files = sorted(
[
f
for f in os.listdir(full_path)
if f.endswith(".md") and os.path.isfile(os.path.join(full_path, f))
]
)
for filename in md_files[:max_files]:
file_full_path = os.path.join(full_path, filename)
relative_path = os.path.join(dir_path, filename)
try:
with open(file_full_path, "r", encoding="utf-8") as f:
content = f.read()
results.append((relative_path, content))
except Exception:
continue
except Exception:
pass
return results
def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]:
"""
Read all file/directory contents referenced in jsonl file
Schema:
{"file": "path/to/file.md", "reason": "..."}
{"file": "path/to/dir/", "type": "directory", "reason": "..."}
{"_example": "..."} # seed row — 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.
Returns:
[(path, content), ...]
"""
full_path = os.path.join(base_path, jsonl_path)
if not os.path.exists(full_path):
print(
f"[inject-subagent-context] WARN: {jsonl_path} not found — "
f"sub-agent will receive only prd.md",
file=sys.stderr,
)
return []
results = []
saw_real_entry = False
try:
with open(full_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
item = json.loads(line)
file_path = item.get("file") or item.get("path")
entry_type = item.get("type", "file")
if not file_path:
# Seed / comment row — skip silently
continue
saw_real_entry = True
if entry_type == "directory":
# Read all .md files in directory
dir_contents = read_directory_contents(base_path, file_path)
results.extend(dir_contents)
else:
# Read single file
content = read_file_content(base_path, file_path)
if content:
results.append((file_path, content))
except json.JSONDecodeError:
continue
except Exception:
pass
if not saw_real_entry:
print(
f"[inject-subagent-context] WARN: {jsonl_path} has no curated "
f"entries (only seed / empty) — sub-agent will receive only "
f"prd.md. See workflow.md Phase 1.3 for curation guidance.",
file=sys.stderr,
)
return results
def get_agent_context(repo_root: str, task_dir: str, agent_type: str) -> str:
"""
Get context from {agent_type}.jsonl for the specified agent.
Only reads implement.jsonl or check.jsonl (the two JSONL files the task system creates).
"""
context_parts = []
agent_jsonl = f"{task_dir}/{agent_type}.jsonl"
for file_path, content in read_jsonl_entries(repo_root, agent_jsonl):
context_parts.append(f"=== {file_path} ===\n{content}")
return "\n\n".join(context_parts)
def get_implement_context(repo_root: str, task_dir: str) -> str:
"""
Complete context for Implement Agent
Read order:
1. All files in implement.jsonl (dev specs)
2. prd.md (requirements)
3. info.md (technical design)
"""
context_parts = []
# 1. Read implement.jsonl
base_context = get_agent_context(repo_root, task_dir, "implement")
if base_context:
context_parts.append(base_context)
# 2. Requirements document
prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
if prd_content:
context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
# 3. Technical design
info_content = read_file_content(repo_root, f"{task_dir}/info.md")
if info_content:
context_parts.append(
f"=== {task_dir}/info.md (Technical Design) ===\n{info_content}"
)
return "\n\n".join(context_parts)
def get_check_context(repo_root: str, task_dir: str) -> str:
"""
Context for Check Agent: check.jsonl + prd.md
"""
context_parts = []
for file_path, content in read_jsonl_entries(repo_root, f"{task_dir}/check.jsonl"):
context_parts.append(f"=== {file_path} ===\n{content}")
prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
if prd_content:
context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
return "\n\n".join(context_parts)
def get_finish_context(repo_root: str, task_dir: str) -> str:
"""
Context for Finish phase: reuses check.jsonl + prd.md
(Finish is a final check, same context source.)
"""
return get_check_context(repo_root, task_dir)
def build_implement_prompt(original_prompt: str, context: str) -> str:
"""Build complete prompt for Implement"""
return f"""<!-- trellis-hook-injected -->
# Implement Agent Task
You are the Implement Agent in the Multi-Agent Pipeline.
## Your Context
All the information you need has been prepared for you:
{context}
---
## Your Task
{original_prompt}
---
## Workflow
1. **Understand specs** - All dev specs are injected above, understand them
2. **Understand requirements** - Read requirements document and technical design
3. **Implement feature** - Implement following specs and design
4. **Self-check** - Ensure code quality against check specs
## Important Constraints
- Do NOT execute git commit, only code modifications
- Follow all dev specs injected above
- Report list of modified/created files when done"""
def build_check_prompt(original_prompt: str, context: str) -> str:
"""Build complete prompt for Check"""
return f"""<!-- trellis-hook-injected -->
# Check Agent Task
You are the Check Agent in the Multi-Agent Pipeline (code and cross-layer checker).
## Your Context
All check specs and dev specs you need:
{context}
---
## Your Task
{original_prompt}
---
## Workflow
1. **Get changes** - Run `git diff --name-only` and `git diff` to get code changes
2. **Check against specs** - Check item by item against specs above
3. **Self-fix** - Fix issues directly, don't just report
4. **Run verification** - Run project's lint and typecheck commands
## Important Constraints
- Fix issues yourself, don't just report
- Must execute complete checklist in check specs
- Pay special attention to impact radius analysis (L1-L5)"""
def build_finish_prompt(original_prompt: str, context: str) -> str:
"""Build complete prompt for Finish (final check before PR)"""
return f"""<!-- trellis-hook-injected -->
# Finish Agent Task
You are performing the final check before creating a PR.
## Your Context
Finish checklist and requirements:
{context}
---
## Your Task
{original_prompt}
---
## Workflow
1. **Review changes** - Run `git diff --name-only` to see all changed files
2. **Verify requirements** - Check each requirement in prd.md is implemented
3. **Spec sync** - Analyze whether changes introduce new patterns, contracts, or conventions
- If new pattern/convention found: read target spec file → update it → update index.md if needed
- If infra/cross-layer change: follow the 7-section mandatory template from update-spec.md
- If pure code fix with no new patterns: skip this step
4. **Run final checks** - Execute lint and typecheck
5. **Confirm ready** - Ensure code is ready for PR
## Important Constraints
- You MAY update spec files when gaps are detected (use update-spec.md as guide)
- MUST read the target spec file BEFORE editing (avoid duplicating existing content)
- Do NOT update specs for trivial changes (typos, formatting, obvious fixes)
- If critical CODE issues found, report them clearly (fix specs, not code)
- Verify all acceptance criteria in prd.md are met"""
def get_research_context(repo_root: str, task_dir: str | None) -> str:
"""
Context for Research Agent — project structure overview for spec directories.
`task_dir` kept for signature parity with get_implement_context / get_check_context
so the dispatcher can call them uniformly.
"""
_ = task_dir
context_parts = []
# 1. Project structure overview (dynamically discover spec directories)
spec_path = f"{DIR_WORKFLOW}/{DIR_SPEC}"
spec_root = Path(repo_root) / DIR_WORKFLOW / DIR_SPEC
# Build spec tree dynamically
tree_lines = [f"{spec_path}/"]
if spec_root.is_dir():
pkg_dirs = sorted(d for d in spec_root.iterdir() if d.is_dir())
for i, pkg_dir in enumerate(pkg_dirs):
is_last = i == len(pkg_dirs) - 1
prefix = "└── " if is_last else "├── "
layers = sorted(d.name for d in pkg_dir.iterdir() if d.is_dir())
layer_info = f" ({', '.join(layers)})" if layers else ""
tree_lines.append(f"{prefix}{pkg_dir.name}/{layer_info}")
spec_tree = "\n".join(tree_lines)
project_structure = f"""## Project Spec Directory Structure
```
{spec_tree}
```
To get structured package info, run: `python ./{DIR_WORKFLOW}/scripts/get_context.py --mode packages`
## Search Tips
- Spec files: `{spec_path}/**/*.md`
- Code search: Use Glob and Grep tools
- Tech solutions: Use mcp__exa__web_search_exa or mcp__exa__get_code_context_exa"""
context_parts.append(project_structure)
return "\n\n".join(context_parts)
def build_research_prompt(original_prompt: str, context: str) -> str:
"""Build complete prompt for Research"""
return f"""# Research Agent Task
You are the Research Agent in the Multi-Agent Pipeline (search researcher).
## Core Principle
**You do one thing: find and explain information.**
You are a documenter, not a reviewer.
## Project Info
{context}
---
## Your Task
{original_prompt}
---
## Workflow
1. **Understand query** - Determine search type (internal/external) and scope
2. **Plan search** - List search steps for complex queries
3. **Execute search** - Execute multiple independent searches in parallel
4. **Organize results** - Output structured report
## Search Tools
| Tool | Purpose |
|------|---------|
| Glob | Search by filename pattern |
| Grep | Search by content |
| Read | Read file content |
| mcp__exa__web_search_exa | External web search |
| mcp__exa__get_code_context_exa | External code/doc search |
## Strict Boundaries
**Only allowed**: Describe what exists, where it is, how it works
**Forbidden** (unless explicitly asked):
- Suggest improvements
- Criticize implementation
- Recommend refactoring
- Modify any files
## Report Format
Provide structured search results including:
- List of files found (with paths)
- Code pattern analysis (if applicable)
- Related spec documents
- External references (if any)"""
def _string_value(value: Any) -> str:
if isinstance(value, str):
stripped = value.strip()
return stripped
return ""
def _extract_subagent_name(value: Any) -> str:
"""Extract a sub-agent name from common platform encodings.
Cursor's native Task args encode custom sub-agents as a protobuf oneof,
which can appear in hook JSON as either ``{"custom": {"name": "..."}}``
or ``{"type": {"case": "custom", "value": {"name": "..."}}}``.
"""
direct = _string_value(value)
if direct:
return direct
if not isinstance(value, dict):
return ""
for key in ("name", "subagent_type_name", "subagentTypeName"):
direct = _string_value(value.get(key))
if direct:
return direct
custom = value.get("custom")
if isinstance(custom, dict):
custom_name = _string_value(custom.get("name"))
if custom_name:
return custom_name
oneof = value.get("type")
if isinstance(oneof, dict):
case_name = _string_value(oneof.get("case"))
if case_name == "custom":
nested_value = oneof.get("value")
if isinstance(nested_value, dict):
custom_name = _string_value(nested_value.get("name"))
if custom_name:
return custom_name
if case_name:
return case_name
case_name = _string_value(value.get("case"))
if case_name == "custom":
nested_value = value.get("value")
if isinstance(nested_value, dict):
custom_name = _string_value(nested_value.get("name"))
if custom_name:
return custom_name
if case_name:
return case_name
for agent_name in AGENTS_ALL:
if agent_name in value:
return agent_name
return ""
def _extract_subagent_type(tool_input: dict) -> str:
for key in (
"subagent_type",
"subagentType",
"subagent_type_name",
"subagentTypeName",
"agent_type",
"agentType",
"name",
):
agent_name = _extract_subagent_name(tool_input.get(key))
if agent_name:
return agent_name
return ""
def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]:
"""Parse hook input across different platform formats.
Returns (subagent_type, original_prompt, tool_input).
Handles:
- Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type
- Cursor: tool_name=Task|Subagent, tool_input.subagent_type
- Copilot CLI: toolName=task (camelCase key, lowercase value)
- Gemini CLI: tool_name IS the agent name (BeforeTool matcher already filtered)
- Kiro: agentSpawn hook, agent_name field at top level
"""
tool_input = input_data.get("tool_input", {})
# Standard format: Task/Agent tool with subagent_type
tool_name = input_data.get("tool_name", "") or input_data.get("toolName", "")
if tool_name.lower() in ("task", "agent", "subagent"):
return (
_extract_subagent_type(tool_input),
tool_input.get("prompt", ""),
tool_input,
)
# Kiro: agentSpawn hook passes agent_name at top level
agent_name = input_data.get("agent_name", "")
if agent_name:
return agent_name, tool_input.get("prompt", input_data.get("prompt", "")), tool_input
# Gemini CLI: BeforeTool where tool_name IS the agent name
# (matcher already ensured it's one of our agents)
if tool_name in AGENTS_ALL:
return tool_name, tool_input.get("prompt", ""), tool_input
# Copilot CLI: toolName field (camelCase), value might be the agent name
tool_name_camel = input_data.get("toolName", "")
if tool_name_camel in AGENTS_ALL:
return tool_name_camel, input_data.get("toolArgs", ""), tool_input
return "", "", tool_input
def main():
if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
sys.exit(0)
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError:
sys.exit(0)
subagent_type, original_prompt, tool_input = _parse_hook_input(input_data)
cwd = input_data.get("cwd", os.getcwd())
# Only handle subagent types we care about
if subagent_type not in AGENTS_ALL:
sys.exit(0)
# Find repo root
repo_root = find_repo_root(cwd)
if not repo_root:
sys.exit(0)
# Get current task directory (research doesn't require it)
task_dir = get_current_task(repo_root, input_data)
# implement/check need task directory
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)
if not os.path.exists(task_dir_full):
sys.exit(0)
# Check for [finish] marker in prompt (check agent with finish context)
is_finish_phase = "[finish]" in original_prompt.lower()
# Get context and build prompt based on subagent type
if subagent_type == AGENT_IMPLEMENT:
assert task_dir is not None # validated above
context = get_implement_context(repo_root, task_dir)
new_prompt = build_implement_prompt(original_prompt, context)
elif subagent_type == AGENT_CHECK:
assert task_dir is not None # validated above
if is_finish_phase:
# Finish phase: use finish context (lighter, focused on final verification)
context = get_finish_context(repo_root, task_dir)
new_prompt = build_finish_prompt(original_prompt, context)
else:
# Regular check phase: use check context (full specs for self-fix loop)
context = get_check_context(repo_root, task_dir)
new_prompt = build_check_prompt(original_prompt, context)
elif subagent_type == AGENT_RESEARCH:
# Research can work without task directory
context = get_research_context(repo_root, task_dir)
new_prompt = build_research_prompt(original_prompt, context)
else:
sys.exit(0)
if not context:
sys.exit(0)
# Return updated input — use a multi-format output that covers all platforms.
# Most platforms ignore unrecognized fields, so we include multiple formats.
# The platform picks whichever fields it understands.
updated = {**tool_input, "prompt": new_prompt}
output = {
# Claude Code / Qoder / CodeBuddy / Droid format
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": updated,
},
# Cursor format
"permission": "allow",
"updated_input": updated,
# Gemini format
"updatedInput": updated,
}
print(json.dumps(output, ensure_ascii=False))
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""Trellis per-turn breadcrumb hook (UserPromptSubmit / BeforeAgent equivalent).
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.
The emitted ``hookEventName`` field is platform-aware: most hosts expect
``UserPromptSubmit`` (Claude Code naming, also accepted by Cursor / Qoder /
CodeBuddy / Droid / Codex / Copilot wiring), but Gemini CLI 0.40.x renamed
its per-turn event to ``BeforeAgent`` and its schema validator rejects the
legacy name. ``_detect_platform`` picks the right value at runtime.
Breadcrumb text is pulled exclusively from workflow.md
[workflow-state:STATUS] tag blocks — workflow.md is the single source of
truth. There are no fallback dicts in this script: when workflow.md is
missing or a tag is absent, the breadcrumb degrades to a generic
"Refer to workflow.md for current step." line so users see (and fix)
the broken state instead of the hook silently masking it.
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
"""
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,
)
def load_breadcrumbs(root: Path) -> dict[str, str]:
"""Parse workflow.md for [workflow-state:STATUS] blocks.
Returns {status: body_text}. workflow.md is the single source of
truth — there are no fallback dicts in this script. Missing tags
(or a missing/unreadable workflow.md) fall back to a generic line
in build_breadcrumb so users see the broken state and fix
workflow.md, rather than the hook silently masking the issue.
"""
workflow = root / ".trellis" / "workflow.md"
if not workflow.is_file():
return {}
try:
content = workflow.read_text(encoding="utf-8")
except OSError:
return {}
result: dict[str, str] = {}
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 (tag present in workflow.md) → detailed template body
- Unknown status (no tag, or workflow.md missing) → generic
"Refer to workflow.md for current step." line
- `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:
if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
return 0
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)
# Gemini CLI 0.40.x rejects "UserPromptSubmit" — its per-turn event is
# named "BeforeAgent". Other platforms (Claude/Cursor/Qoder/CodeBuddy/
# Droid/Codex/Copilot) accept the original Claude-style name.
hook_event_name = (
"BeforeAgent" if _detect_platform(data) == "gemini" else "UserPromptSubmit"
)
output = {
"hookSpecificOutput": {
"hookEventName": hook_event_name,
"additionalContext": breadcrumb,
}
}
print(json.dumps(output))
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,780 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Session Start Hook - Inject structured context
"""
from __future__ import annotations
# IMPORTANT: Suppress all warnings FIRST
import warnings
warnings.filterwarnings("ignore")
import json
import os
import re
import shlex
import subprocess
import sys
from io import StringIO
from pathlib import Path
def _normalize_windows_shell_path(path_str: str) -> str:
"""Normalize Unix-style shell paths to real Windows paths.
On Windows, shells like Git Bash / MSYS2 / Cygwin may report paths like
`/d/Users/...` or `/cygdrive/d/Users/...`. `Path.resolve()` will misinterpret
these as `D:/d/Users...` on drive D: (or similar), breaking repo root
detection.
This function is intentionally conservative: it only rewrites patterns that
unambiguously represent a drive letter mount.
"""
if not isinstance(path_str, str) or not path_str:
return path_str
# Only relevant on Windows; keep other platforms untouched.
if not sys.platform.startswith("win"):
return path_str
p = path_str.strip()
# Already a Windows drive path (C:\... or C:/...)
if re.match(r"^[A-Za-z]:[\/]", p):
return p
# MSYS/Git-Bash style: /c/Users/... or /d/Work/...
m = re.match(r"^/([A-Za-z])/(.*)", p)
if m:
drive, rest = m.group(1).upper(), m.group(2)
rest = rest.replace('/', '\\')
return f"{drive}:\\{rest}"
# Cygwin style: /cygdrive/c/Users/...
m = re.match(r"^/cygdrive/([A-Za-z])/(.*)", p)
if m:
drive, rest = m.group(1).upper(), m.group(2)
rest = rest.replace('/', '\\')
return f"{drive}:\\{rest}"
# WSL mounted drive (sometimes leaked into env): /mnt/c/Users/...
m = re.match(r"^/mnt/([A-Za-z])/(.*)", p)
if m:
drive, rest = m.group(1).upper(), m.group(2)
rest = rest.replace('/', '\\')
return f"{drive}:\\{rest}"
return path_str
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>"""
# IMPORTANT: Force stdout to use UTF-8 on Windows
# This fixes UnicodeEncodeError when outputting non-ASCII characters
if sys.platform.startswith("win"):
import io as _io
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
elif hasattr(sys.stdout, "detach"):
sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr]
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 hook-inject and pull-based
sub-agent context loaders.
"""
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 should_skip_injection() -> bool:
"""Check if any platform's non-interactive flag is set, or if Trellis
hooks are explicitly disabled via TRELLIS_HOOKS=0 / TRELLIS_DISABLE_HOOKS=1.
"""
if os.environ.get("TRELLIS_HOOKS") == "0":
return True
if os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
return True
non_interactive_vars = [
"CLAUDE_NON_INTERACTIVE",
"QODER_NON_INTERACTIVE",
"CODEBUDDY_NON_INTERACTIVE",
"FACTORY_NON_INTERACTIVE",
"CURSOR_NON_INTERACTIVE",
"GEMINI_NON_INTERACTIVE",
"KIRO_NON_INTERACTIVE",
"COPILOT_NON_INTERACTIVE",
]
return any(os.environ.get(var) == "1" for var in non_interactive_vars)
def read_file(path: Path, fallback: str = "") -> str:
try:
return path.read_text(encoding="utf-8")
except (FileNotFoundError, PermissionError):
return fallback
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_context_key(trellis_dir: Path, input_data: dict) -> str | None:
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_context_key # type: ignore[import-not-found]
return resolve_context_key(input_data, platform=_detect_platform(input_data))
def _persist_context_key_for_bash(context_key: str | None) -> None:
"""Expose Trellis session identity to later Claude Code Bash commands.
Claude Code SessionStart hooks can append exports to CLAUDE_ENV_FILE; those
variables are then available to Bash tools in the same conversation. Without
this bridge, `task.py start` has hook stdin during SessionStart but no
session identity when the AI later runs it as a normal shell command.
"""
if not context_key:
return
env_file = os.environ.get("CLAUDE_ENV_FILE")
if not env_file:
return
try:
with open(env_file, "a", encoding="utf-8") as handle:
handle.write(f"export TRELLIS_CONTEXT_ID={shlex.quote(context_key)}\n")
except OSError:
pass
def _resolve_active_task(trellis_dir: Path, input_data: 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,
input_data,
platform=_detect_platform(input_data),
)
def run_script(script_path: Path, context_key: str | None = None) -> str:
try:
if script_path.suffix == ".py":
# Add PYTHONIOENCODING to force UTF-8 in subprocess
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)]
else:
env = os.environ.copy()
if context_key:
env["TRELLIS_CONTEXT_ID"] = context_key
cmd = [str(script_path)]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
cwd=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, input_data: dict) -> str:
"""Check current task status and return structured status string with explicit next action.
Returns a block with three fields:
- Status: current state
- Task: task identifier (when applicable)
- Next-Action: explicit skill/command/tool call the AI should invoke
"""
active = _resolve_active_task(trellis_dir, input_data)
# Case 1: No active task — waiting for user to describe intent
if not active.task_path:
return (
"Status: NO ACTIVE TASK\n"
f"Source: {active.source}\n"
"Next-Action: After the user describes their intent, load skill `trellis-brainstorm` "
"to clarify requirements and create a task via `python ./.trellis/scripts/task.py create`.\n"
"Research reminder: for research-heavy tasks (comparing tools, reading external docs, "
"cross-platform surveys), spawn `trellis-research` sub-agents via the Task tool — "
"they persist findings to `{TASK_DIR}/research/*.md` and keep main context clean. "
"Do NOT do 10+ inline WebFetch/WebSearch in the main conversation.\n"
"User override (per-turn escape hatch): if the user's first message explicitly opts "
"out of the workflow (\"跳过 trellis\" / \"别走流程\" / \"小修一下\" / \"直接改\" / "
"\"skip trellis\" / \"no task\" / \"just do it\"), honor it for this turn — "
"acknowledge briefly and proceed without creating a task. Per-turn only."
)
# Case 2: Stale pointer — task dir was deleted
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}\n"
f"Source: {active.source}\n"
f"Next-Action: Run `python ./.trellis/scripts/task.py finish` to clear the stale pointer, "
"then ask the user what to work on next."
)
# Read task.json
task_json_path = task_dir / "task.json"
task_data = {}
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")
# Case 3: Task completed — time to archive
if task_status == "completed":
return (
f"Status: COMPLETED\nTask: {task_title}\n"
f"Source: {active.source}\n"
f"Next-Action: Load skill `trellis-update-spec` to capture learnings, "
f"then archive with `python ./.trellis/scripts/task.py archive {task_dir.name}`."
)
has_prd = (task_dir / "prd.md").is_file()
# Case 4: No PRD — still in Plan phase
if not has_prd:
return (
f"Status: PLANNING\nTask: {task_title}\n"
f"Source: {active.source}\n"
"Next-Action: Load skill `trellis-brainstorm` to clarify requirements with the user "
"and produce prd.md in the task directory.\n"
"Research reminder: when the task needs external research (tool comparison, docs, "
"conventions survey), spawn `trellis-research` sub-agents — don't WebFetch/WebSearch "
"inline in the main session. Findings go to `{task_dir}/research/*.md`; PRD only links to them."
)
# Case 4b: PRD exists but implement.jsonl has only seed (no curated entries) — Phase 1.3 gate
implement_jsonl = task_dir / "implement.jsonl"
if implement_jsonl.is_file() and not _has_curated_jsonl_entry(implement_jsonl):
return (
f"Status: PLANNING (Phase 1.3)\nTask: {task_title}\n"
f"Source: {active.source}\n"
"Next-Action: Curate `implement.jsonl` and `check.jsonl` with the spec + research files "
"the Phase 2 sub-agents will need. Only spec paths (`.trellis/spec/**/*.md`) and research "
"files (`{TASK_DIR}/research/*.md`) — no code paths. Run "
"`python ./.trellis/scripts/get_context.py --mode packages` to list available specs, "
"then edit the jsonl files or use `python ./.trellis/scripts/task.py add-context`. "
"See `.trellis/workflow.md` Phase 1.3 for details."
)
# Case 5: PRD + curated jsonl (or agent-less platform with no jsonl) — enter Execute phase
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"
"Sub-agent roster: `trellis-implement` (writes code), `trellis-check` (verifies + self-fixes), "
"`trellis-research` (persists findings to `research/*.md` — use when you'd otherwise do "
"multiple WebFetch/WebSearch inline).\n"
"Sub-agent self-exemption: if you are reading this as a `trellis-implement` or "
"`trellis-check` sub-agent (your own role / agent name reflects that), this dispatch "
"instruction does NOT apply to you — you are already the dispatched sub-agent. "
"Implement / check directly without spawning another sub-agent of the same kind.\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 _load_trellis_config(trellis_dir: Path, input_data: dict) -> tuple:
"""Load Trellis config for session-start decisions.
Returns:
(is_mono, packages_dict, spec_scope, task_pkg, default_pkg)
"""
scripts_dir = trellis_dir / "scripts"
if str(scripts_dir) not in sys.path:
sys.path.insert(0, str(scripts_dir))
try:
from common.config import get_default_package, get_packages, get_spec_scope, is_monorepo # type: ignore[import-not-found]
from common.paths import get_current_task # type: ignore[import-not-found]
repo_root = trellis_dir.parent
is_mono = is_monorepo(repo_root)
packages = get_packages(repo_root) or {}
scope = get_spec_scope(repo_root)
# Get active task's package
task_pkg = None
current = get_current_task(
repo_root,
input_data,
platform=_detect_platform(input_data),
)
if current:
task_json = repo_root / current / "task.json"
if task_json.is_file():
try:
data = json.loads(task_json.read_text(encoding="utf-8"))
if isinstance(data, dict):
tp = data.get("package")
if isinstance(tp, str) and tp:
task_pkg = tp
except (json.JSONDecodeError, OSError):
pass
default_pkg = get_default_package(repo_root)
return is_mono, packages, scope, task_pkg, default_pkg
except Exception:
return False, {}, None, None, None
def _check_legacy_spec(trellis_dir: Path, is_mono: bool, packages: dict) -> str | None:
"""Check for legacy spec directory structure in monorepo.
Returns warning message if legacy structure detected, None otherwise.
"""
if not is_mono or not packages:
return None
spec_dir = trellis_dir / "spec"
if not spec_dir.is_dir():
return None
# Check for legacy flat spec dirs (spec/backend/, spec/frontend/ with index.md)
has_legacy = False
for legacy_name in ("backend", "frontend"):
legacy_dir = spec_dir / legacy_name
if legacy_dir.is_dir() and (legacy_dir / "index.md").is_file():
has_legacy = True
break
if not has_legacy:
return None
# Check which packages are missing spec/<pkg>/ directory
missing = [
name for name in sorted(packages.keys())
if not (spec_dir / name).is_dir()
]
if not missing:
return None # All packages have spec dirs
if len(missing) == len(packages):
return (
f"[!] Legacy spec structure detected: found `spec/backend/` or `spec/frontend/` "
f"but no package-scoped `spec/<package>/` directories.\n"
f"Monorepo packages: {', '.join(sorted(packages.keys()))}\n"
f"Please reorganize: `spec/backend/` -> `spec/<package>/backend/`"
)
return (
f"[!] Partial spec migration detected: packages {', '.join(missing)} "
f"still missing `spec/<pkg>/` directory.\n"
f"Please complete migration for all packages."
)
def _resolve_spec_scope(
is_mono: bool,
packages: dict,
scope,
task_pkg: str | None,
default_pkg: str | None,
) -> set | None:
"""Resolve which packages should have their specs injected.
Returns:
Set of package names to include, or None for full scan.
"""
if not is_mono or not packages:
return None # Single-repo: full scan
if scope is None:
return None # No scope configured: full scan
if isinstance(scope, str) and scope == "active_task":
if task_pkg and task_pkg in packages:
return {task_pkg}
if default_pkg and default_pkg in packages:
return {default_pkg}
return None # Fallback to full scan
if isinstance(scope, list):
valid = set()
for entry in scope:
if entry in packages:
valid.add(entry)
else:
print(
f"Warning: spec_scope contains unknown package: {entry}, ignoring",
file=sys.stderr,
)
if valid:
# Warn if active task is out of scope
if task_pkg and task_pkg not in valid:
print(
f"Warning: active task package '{task_pkg}' is out of configured spec_scope",
file=sys.stderr,
)
return valid
# All entries invalid: fallback chain
print(
"Warning: all spec_scope entries invalid, falling back to task/default/full",
file=sys.stderr,
)
if task_pkg and task_pkg in packages:
return {task_pkg}
if default_pkg and default_pkg in packages:
return {default_pkg}
return None # Full scan
return None # Unknown scope type: full scan
def _extract_range(content: str, start_header: str, end_header: str) -> str:
"""Extract lines starting at `## start_header` up to (but excluding) `## end_header`.
Both parameters are full header lines WITHOUT the `## ` prefix (e.g. "Phase Index").
Returns empty string if start header is not found.
End header missing → extracts to end of file.
"""
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()
_BREADCRUMB_TAG_RE = re.compile(
r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]",
re.DOTALL,
)
def _strip_breadcrumb_tag_blocks(content: str) -> str:
"""Remove `[workflow-state:STATUS]...[/workflow-state:STATUS]` blocks.
The tag blocks live inside `## Phase Index` (since v0.5.0-rc.0, when
they were colocated with their phase summaries) and are consumed by the
UserPromptSubmit hook (`inject-workflow-state.py`). The session-start
payload already covers the full step bodies, so re-inlining the
breadcrumbs here would just duplicate context.
"""
return _BREADCRUMB_TAG_RE.sub("", content)
def _build_workflow_overview(workflow_path: Path) -> str:
"""Inject the workflow guide for the session.
Contents:
1. Section index (all `## ` headings — navigation)
2. Phase Index section (rules, skill routing table, anti-rationalization table)
3. Phase 1/2/3 step-level details (the actual how-to for each step)
The meta sections (Core Principles / Trellis System / Customizing
Trellis) are NOT injected — Core Principles is short prose the AI can
Read on demand; Trellis System lists reference commands duplicated in
step bodies; Customizing Trellis is for forks. Workflow-state breadcrumb
tag blocks (which now live inside Phase Index since v0.5.0-rc.0) are
stripped from the extracted range — they're consumed by the
UserPromptSubmit hook, not the session-start preamble.
Total budget: Phase Index ~2 KB + Phase 1/2/3 ~7 KB = ~9 KB.
"""
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 += ["", "---", ""]
# Extract Phase Index through the end of Phase 3 (before "Customizing
# Trellis" — the docs-for-forks footer added in v0.5.0-rc.0). Since
# sections appear in order Phase Index → Phase 1 → Phase 2 → Phase 3 →
# Customizing Trellis, a single range grab captures all four. The
# breadcrumb tag blocks now embedded inside Phase Index are stripped so
# they don't duplicate the per-turn UserPromptSubmit injection.
phases = _extract_range(
content, "Phase Index", "Customizing Trellis (for forks)"
)
if phases:
out_lines.append(_strip_breadcrumb_tag_blocks(phases).rstrip())
return "\n".join(out_lines).rstrip()
def main():
if should_skip_injection():
sys.exit(0)
try:
hook_input = json.loads(sys.stdin.read())
if not isinstance(hook_input, dict):
hook_input = {}
except (json.JSONDecodeError, ValueError):
hook_input = {}
# Try platform-specific env vars, hook cwd, fallback to cwd
project_dir_env_vars = [
"CLAUDE_PROJECT_DIR",
"QODER_PROJECT_DIR",
"CODEBUDDY_PROJECT_DIR",
"FACTORY_PROJECT_DIR",
"CURSOR_PROJECT_DIR",
"GEMINI_PROJECT_DIR",
"KIRO_PROJECT_DIR",
"COPILOT_PROJECT_DIR",
]
project_dir = None
for var in project_dir_env_vars:
val = os.environ.get(var)
if val:
project_dir = Path(_normalize_windows_shell_path(val)).resolve()
break
if project_dir is None:
project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve()
trellis_dir = project_dir / ".trellis"
context_key = _resolve_context_key(trellis_dir, hook_input)
_persist_context_key_for_bash(context_key)
# Load config for scope filtering and legacy detection
is_mono, packages, scope_config, task_pkg, default_pkg = _load_trellis_config(
trellis_dir,
hook_input,
)
allowed_pkgs = _resolve_spec_scope(is_mono, packages, scope_config, task_pkg, default_pkg)
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")
# Legacy migration warning
legacy_warning = _check_legacy_spec(trellis_dir, is_mono, packages)
if legacy_warning:
output.write(f"<migration-warning>\n{legacy_warning}\n</migration-warning>\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_overview(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 "
"or loaded by the sub-agent 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"
"- Sub-agent self-exemption: if you are reading this as a `trellis-implement` "
"or `trellis-check` sub-agent, the \"dispatch trellis-implement / trellis-check\" "
"rule above does NOT apply to you — you are already the dispatched sub-agent. "
"Do NOT spawn another sub-agent of the same kind; implement / check directly.\n\n"
)
# guides/ is cross-package thinking — always include inline (small, 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 spec indexes — paths only (main agent reads on demand;
# sub-agents get their specific specs via jsonl injection)
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 # already inlined above
index_file = sub / "index.md"
if index_file.is_file():
# Flat spec dir (single-repo layer like spec/backend/)
paths.append(f".trellis/spec/{sub.name}/index.md")
else:
# Nested package dirs (monorepo: spec/<pkg>/<layer>/index.md)
# Apply scope filter
if allowed_pkgs is not None and sub.name not in allowed_pkgs:
continue
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: "
"`python ./.trellis/scripts/get_context.py --mode packages`\n"
)
output.write("</guidelines>\n\n")
# Check task status and inject structured tag
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>""")
result = {
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": output.getvalue(),
}
}
# Output JSON - stdout is already configured for UTF-8
print(json.dumps(result, ensure_ascii=False), flush=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,34 @@
---
name: trellis-before-dev
description: "Discovers and injects project-specific coding guidelines from .trellis/spec/ before implementation begins. Reads spec indexes, pre-development checklists, and shared thinking guides for the target package. Use when starting a new coding task, before writing any code, switching to a different package, or needing to refresh project conventions and standards."
---
Read the relevant development guidelines before starting your task.
Execute these steps:
1. **Discover packages and their spec layers**:
```bash
python ./.trellis/scripts/get_context.py --mode packages
```
2. **Identify which specs apply** to your task based on:
- Which package you're modifying (e.g., `cli/`, `docs-site/`)
- What type of work (backend, frontend, unit-test, docs, etc.)
3. **Read the spec index** for each relevant module:
```bash
cat .trellis/spec/<package>/<layer>/index.md
```
Follow the **"Pre-Development Checklist"** section in the index.
4. **Read the specific guideline files** listed in the Pre-Development Checklist that are relevant to your task. The index is NOT the goal — it points you to the actual guideline files (e.g., `error-handling.md`, `conventions.md`, `mock-strategies.md`). Read those files to understand the coding standards and patterns.
5. **Always read shared guides**:
```bash
cat .trellis/spec/guides/index.md
```
6. Understand the coding standards and patterns you need to follow, then proceed with your development plan.
This step is **mandatory** before writing any code.

View File

@@ -0,0 +1,538 @@
---
name: trellis-brainstorm
description: "Guides collaborative requirements discovery before implementation. Creates task directory, seeds PRD, asks high-value questions one at a time, researches technical choices, and converges on MVP scope. Use when requirements are unclear, there are multiple valid approaches, or the user describes a new feature or complex task."
---
# Brainstorm - Requirements Discovery (AI Coding Enhanced)
Guide AI through collaborative requirements discovery **before implementation**, optimized for AI coding workflows:
* **Task-first** (capture ideas immediately)
* **Action-before-asking** (reduce low-value questions)
* **Research-first** for technical choices (avoid asking users to invent options)
* **Diverge → Converge** (expand thinking, then lock MVP)
---
## When to Use
Triggered from /trellis-start when the user describes a development task, especially when:
* requirements are unclear or evolving
* there are multiple valid implementation paths
* trade-offs matter (UX, reliability, maintainability, cost, performance)
* the user might not know the best options up front
---
## Core Principles (Non-negotiable)
1. **Task-first (capture early)**
Always ensure a task exists at the start so the user's ideas are recorded immediately.
2. **Action before asking**
If you can derive the answer from repo code, docs, configs, conventions, or quick research — do that first.
3. **One question per message**
Never overwhelm the user with a list of questions. Ask one, update PRD, repeat.
4. **Prefer concrete options**
For preference/decision questions, present 23 feasible, specific approaches with trade-offs.
5. **Research-first for technical choices**
If the decision depends on industry conventions / similar tools / established patterns, do research first, then propose options.
6. **Diverge → Converge**
After initial understanding, proactively consider future evolution, related scenarios, and failure/edge cases — then converge to an MVP with explicit out-of-scope.
7. **No meta questions**
Do not ask "should I search?" or "can you paste the code so I can continue?"
If you need information: search/inspect. If blocked: ask the minimal blocking question.
---
## Step 0: Ensure Task Exists (ALWAYS)
Before any Q&A, ensure a task exists. If none exists, create one immediately.
* Use a **temporary working title** derived from the user's message.
* It's OK if the title is imperfect — refine later in PRD.
```bash
TASK_DIR=$(python ./.trellis/scripts/task.py create "brainstorm: <short goal>" --slug <auto>)
```
Use a slug without a date prefix. `task.py create` adds the `MM-DD-`
directory prefix automatically.
Create/seed `prd.md` immediately with what you know:
```markdown
# brainstorm: <short goal>
## Goal
<one paragraph: what + why>
## What I already know
* <facts from user message>
* <facts discovered from repo/docs>
## Assumptions (temporary)
* <assumptions to validate>
## Open Questions
* <ONLY Blocking / Preference questions; keep list short>
## Requirements (evolving)
* <start with what is known>
## Acceptance Criteria (evolving)
* [ ] <testable criterion>
## Definition of Done (team quality bar)
* Tests added/updated (unit/integration where appropriate)
* Lint / typecheck / CI green
* Docs/notes updated if behavior changes
* Rollout/rollback considered if risky
## Out of Scope (explicit)
* <what we will not do in this task>
## Technical Notes
* <files inspected, constraints, links, references>
* <research notes summary if applicable>
```
---
## Step 1: Auto-Context (DO THIS BEFORE ASKING QUESTIONS)
Before asking questions like "what does the code look like?", gather context yourself:
### Repo inspection checklist
* Identify likely modules/files impacted
* Locate existing patterns (similar features, conventions, error handling style)
* Check configs, scripts, existing command definitions
* Note any constraints (runtime, dependency policy, build tooling)
### Documentation checklist
* Look for existing PRDs/specs/templates
* Look for command usage examples, README, ADRs if any
Write findings into PRD:
* Add to `What I already know`
* Add constraints/links to `Technical Notes`
---
## Step 2: Classify Complexity (still useful, not gating task creation)
| Complexity | Criteria | Action |
| ------------ | ------------------------------------------------------ | ------------------------------------------- |
| **Trivial** | Single-line fix, typo, obvious change | Skip brainstorm, implement directly |
| **Simple** | Clear goal, 12 files, scope well-defined | Ask 1 confirm question, then implement |
| **Moderate** | Multiple files, some ambiguity | Light brainstorm (23 high-value questions) |
| **Complex** | Vague goal, architectural choices, multiple approaches | Full brainstorm |
> Note: Task already exists from Step 0. Classification only affects depth of brainstorming.
---
## Step 3: Question Gate (Ask ONLY high-value questions)
Before asking ANY question, run the following gate:
### Gate A — Can I derive this without the user?
If answer is available via:
* repo inspection (code/config)
* docs/specs/conventions
* quick market/OSS research
**Do not ask.** Fetch it, summarize, update PRD.
### Gate B — Is this a meta/lazy question?
Examples:
* "Should I search?"
* "Can you paste the code so I can proceed?"
* "What does the code look like?" (when repo is available)
**Do not ask.** Take action.
### Gate C — What type of question is it?
* **Blocking**: cannot proceed without user input
* **Preference**: multiple valid choices, depends on product/UX/risk preference
* **Derivable**: should be answered by inspection/research
→ Only ask **Blocking** or **Preference**.
---
## Step 4: Research-first Mode (Mandatory for technical choices)
### Trigger conditions (any → research-first)
* The task involves selecting an approach, library, protocol, framework, template system, plugin mechanism, or CLI UX convention
* The user asks for "best practice", "how others do it", "recommendation"
* The user can't reasonably enumerate options
### Delegate to `trellis-research` sub-agent (don't research inline)
For each research topic, **spawn a `trellis-research` sub-agent via the Task tool** — don't do WebFetch / WebSearch / `gh api` inline in the main conversation.
Why:
- The sub-agent has its own context window → doesn't pollute brainstorm context with raw tool output
- It persists findings to `{TASK_DIR}/research/<topic>.md` (the contract — see `workflow.md` Phase 1.2)
- It returns only `{file path, one-line summary}` to the main agent
- Independent topics can be **parallelized** — spawn multiple sub-agents in one tool call
Agent type: `trellis-research`
Task description template: "Research <specific question>; persist findings to `{TASK_DIR}/research/<topic-slug>.md`."
❌ Bad (what you must NOT do):
```
Main agent: WebFetch(url-A) → WebFetch(url-B) → Bash(gh api ...)
→ WebSearch(q1) → WebSearch(q2) → ... (10+ inline calls)
→ Write(research/topic.md)
```
→ Pollutes main context with raw HTML/JSON, burns tokens.
✅ Good:
```
Main agent: Task(subagent_type="trellis-research",
prompt="Research topic A; persist to research/topic-a.md")
+ Task(subagent_type="trellis-research",
prompt="Research topic B; persist to research/topic-b.md")
+ Task(subagent_type="trellis-research",
prompt="Research topic C; persist to research/topic-c.md")
→ Reads research/topic-{a,b,c}.md after they finish.
```
### Research steps (to pass into each sub-agent prompt)
Each `trellis-research` sub-agent should:
1. Identify 24 comparable tools/patterns for its topic
2. Summarize common conventions and why they exist
3. Map conventions onto our repo constraints
4. Write findings to `{TASK_DIR}/research/<topic>.md`
Main agent then reads the persisted files and produces **23 feasible approaches** in PRD.
### Research output format (PRD)
The PRD itself should only reference the persisted research files, not duplicate their content. Add a `## Research References` section pointing at `research/*.md`.
Optionally, add a convergence section with feasible approaches derived from the research:
```markdown
## Research References
* [`research/<topic-a>.md`](research/<topic-a>.md) — <one-line takeaway>
* [`research/<topic-b>.md`](research/<topic-b>.md) — <one-line takeaway>
## Research Notes
### What similar tools do
* ...
* ...
### Constraints from our repo/project
* ...
### Feasible approaches here
**Approach A: <name>** (Recommended)
* How it works:
* Pros:
* Cons:
**Approach B: <name>**
* How it works:
* Pros:
* Cons:
**Approach C: <name>** (optional)
* ...
```
Then ask **one** preference question:
* "Which approach do you prefer: A / B / C (or other)?"
---
## Step 5: Expansion Sweep (DIVERGE) — Required after initial understanding
After you can summarize the goal, proactively broaden thinking before converging.
### Expansion categories (keep to 12 bullets each)
1. **Future evolution**
* What might this feature become in 13 months?
* What extension points are worth preserving now?
2. **Related scenarios**
* What adjacent commands/flows should remain consistent with this?
* Are there parity expectations (create vs update, import vs export, etc.)?
3. **Failure & edge cases**
* Conflicts, offline/network failure, retries, idempotency, compatibility, rollback
* Input validation, security boundaries, permission checks
### Expansion message template (to user)
```markdown
I understand you want to implement: <current goal>.
Before diving into design, let me quickly diverge to consider three categories (to avoid rework later):
1. Future evolution: <12 bullets>
2. Related scenarios: <12 bullets>
3. Failure/edge cases: <12 bullets>
For this MVP, which would you like to include (or none)?
1. Current requirement only (minimal viable)
2. Add <X> (reserve for future extension)
3. Add <Y> (improve robustness/consistency)
4. Other: describe your preference
```
Then update PRD:
* What's in MVP → `Requirements`
* What's excluded → `Out of Scope`
---
## Step 6: Q&A Loop (CONVERGE)
### Rules
* One question per message
* Prefer multiple-choice when possible
* After each user answer:
* Update PRD immediately
* Move answered items from `Open Questions``Requirements`
* Update `Acceptance Criteria` with testable checkboxes
* Clarify `Out of Scope`
### Question priority (recommended)
1. **MVP scope boundary** (what is included/excluded)
2. **Preference decisions** (after presenting concrete options)
3. **Failure/edge behavior** (only for MVP-critical paths)
4. **Success metrics & Acceptance Criteria** (what proves it works)
### Preferred question format (multiple choice)
```markdown
For <topic>, which approach do you prefer?
1. **Option A** — <what it means + trade-off>
2. **Option B** — <what it means + trade-off>
3. **Option C** — <what it means + trade-off>
4. **Other** — describe your preference
```
---
## Step 7: Propose Approaches + Record Decisions (Complex tasks)
After requirements are clear enough, propose 23 approaches (if not already done via research-first):
```markdown
Based on current information, here are 23 feasible approaches:
**Approach A: <name>** (Recommended)
* How:
* Pros:
* Cons:
**Approach B: <name>**
* How:
* Pros:
* Cons:
Which direction do you prefer?
```
Record the outcome in PRD as an ADR-lite section:
```markdown
## Decision (ADR-lite)
**Context**: Why this decision was needed
**Decision**: Which approach was chosen
**Consequences**: Trade-offs, risks, potential future improvements
```
---
## Step 8: Final Confirmation + Implementation Plan
When open questions are resolved, confirm complete requirements with a structured summary:
### Final confirmation format
```markdown
Here's my understanding of the complete requirements:
**Goal**: <one sentence>
**Requirements**:
* ...
* ...
**Acceptance Criteria**:
* [ ] ...
* [ ] ...
**Definition of Done**:
* ...
**Out of Scope**:
* ...
**Technical Approach**:
<brief summary + key decisions>
**Implementation Plan (small PRs)**:
* PR1: <scaffolding + tests + minimal plumbing>
* PR2: <core behavior>
* PR3: <edge cases + docs + cleanup>
Does this look correct? If yes, I'll proceed with implementation.
```
### Subtask Decomposition (Complex Tasks)
For complex tasks with multiple independent work items, create subtasks:
```bash
# Create child tasks
CHILD1=$(python ./.trellis/scripts/task.py create "Child task 1" --slug child1 --parent "$TASK_DIR")
CHILD2=$(python ./.trellis/scripts/task.py create "Child task 2" --slug child2 --parent "$TASK_DIR")
# Or link existing tasks
python ./.trellis/scripts/task.py add-subtask "$TASK_DIR" "$CHILD_DIR"
```
---
## PRD Target Structure (final)
`prd.md` should converge to:
```markdown
# <Task Title>
## Goal
<why + what>
## Requirements
* ...
## Acceptance Criteria
* [ ] ...
## Definition of Done
* ...
## Technical Approach
<key design + decisions>
## Decision (ADR-lite)
Context / Decision / Consequences
## Out of Scope
* ...
## Technical Notes
<constraints, references, files, research notes>
```
---
## Anti-Patterns (Hard Avoid)
* Asking user for code/context that can be derived from repo
* Asking user to choose an approach before presenting concrete options
* Meta questions about whether to research
* Staying narrowly on the initial request without considering evolution/edges
* Letting brainstorming drift without updating PRD
---
## Integration with Start Workflow
After brainstorm completes (Step 8 confirmation approved), the flow continues to the Task Workflow's **Phase 2: Prepare for Implementation**:
```text
Brainstorm
Step 0: Create task directory + seed PRD
Step 17: Discover requirements, research, converge
Step 8: Final confirmation → user approves
Task Workflow Phase 2 (Prepare for Implementation)
Code-Spec Depth Check (if applicable)
→ Research codebase (based on confirmed PRD)
→ Configure code-spec context (jsonl files)
→ Activate task
Task Workflow Phase 3 (Execute)
Implement → Check → Complete
```
The task directory and PRD already exist from brainstorm, so Phase 1 of the Task Workflow is skipped entirely.
---
## Related Commands
| Command | When to Use |
|---------|-------------|
| `/trellis-start` | Entry point that triggers brainstorm |
| `/trellis-finish-work` | After implementation is complete |
| `/trellis-update-spec` | If new patterns emerge during work |

View File

@@ -0,0 +1,130 @@
---
name: trellis-break-loop
description: "Deep bug analysis to break the fix-forget-repeat cycle. Analyzes root cause category, why fixes failed, prevention mechanisms, and captures knowledge into specs. Use after fixing a bug to prevent the same class of bugs."
---
# Break the Loop - Deep Bug Analysis
When debug is complete, use this for deep analysis to break the "fix bug -> forget -> repeat" cycle.
---
## Analysis Framework
Analyze the bug you just fixed from these 5 dimensions:
### 1. Root Cause Category
Which category does this bug belong to?
| Category | Characteristics | Example |
|----------|-----------------|---------|
| **A. Missing Spec** | No documentation on how to do it | New feature without checklist |
| **B. Cross-Layer Contract** | Interface between layers unclear | API returns different format than expected |
| **C. Change Propagation Failure** | Changed one place, missed others | Changed function signature, missed call sites |
| **D. Test Coverage Gap** | Unit test passes, integration fails | Works alone, breaks when combined |
| **E. Implicit Assumption** | Code relies on undocumented assumption | Timestamp seconds vs milliseconds |
### 2. Why Fixes Failed (if applicable)
If you tried multiple fixes before succeeding, analyze each failure:
- **Surface Fix**: Fixed symptom, not root cause
- **Incomplete Scope**: Found root cause, didn't cover all cases
- **Tool Limitation**: grep missed it, type check wasn't strict
- **Mental Model**: Kept looking in same layer, didn't think cross-layer
### 3. Prevention Mechanisms
What mechanisms would prevent this from happening again?
| Type | Description | Example |
|------|-------------|---------|
| **Documentation** | Write it down so people know | Update thinking guide |
| **Architecture** | Make the error impossible structurally | Type-safe wrappers |
| **Compile-time** | Strict type checking, no escape hatches | Signature change causes compile error |
| **Runtime** | Monitoring, alerts, scans | Detect orphan entities |
| **Test Coverage** | E2E tests, integration tests | Verify full flow |
| **Code Review** | Checklist, PR template | "Did you check X?" |
### 4. Systematic Expansion
What broader problems does this bug reveal?
- **Similar Issues**: Where else might this problem exist?
- **Design Flaw**: Is there a fundamental architecture issue?
- **Process Flaw**: Is there a development process improvement?
- **Knowledge Gap**: Is the team missing some understanding?
### 5. Knowledge Capture
Solidify insights into the system:
- [ ] Update `.trellis/spec/guides/` thinking guides
- [ ] Update relevant `.trellis/spec/` docs
- [ ] Create issue record (if applicable)
- [ ] Create feature ticket for root fix
- [ ] Update check guidelines if needed
---
## Output Format
Please output analysis in this format:
```markdown
## Bug Analysis: [Short Description]
### 1. Root Cause Category
- **Category**: [A/B/C/D/E] - [Category Name]
- **Specific Cause**: [Detailed description]
### 2. Why Fixes Failed (if applicable)
1. [First attempt]: [Why it failed]
2. [Second attempt]: [Why it failed]
...
### 3. Prevention Mechanisms
| Priority | Mechanism | Specific Action | Status |
|----------|-----------|-----------------|--------|
| P0 | ... | ... | TODO/DONE |
### 4. Systematic Expansion
- **Similar Issues**: [List places with similar problems]
- **Design Improvement**: [Architecture-level suggestions]
- **Process Improvement**: [Development process suggestions]
### 5. Knowledge Capture
- [ ] [Documents to update / tickets to create]
```
---
## Core Philosophy
> **The value of debugging is not in fixing the bug, but in making this class of bugs never happen again.**
Three levels of insight:
1. **Tactical**: How to fix THIS bug
2. **Strategic**: How to prevent THIS CLASS of bugs
3. **Philosophical**: How to expand thinking patterns
30 minutes of analysis saves 30 hours of future debugging.
---
## After Analysis: Immediate Actions
**IMPORTANT**: After completing the analysis above, you MUST immediately:
1. **Update spec/guides** - Don't just list TODOs, actually update the relevant files:
- If it's a cross-platform issue → update `cross-platform-thinking-guide.md`
- If it's a cross-layer issue → update `cross-layer-thinking-guide.md`
- If it's a code reuse issue → update `code-reuse-thinking-guide.md`
- If it's domain-specific → update `backend/*.md` or `frontend/*.md`
2. **Sync templates** - After updating `.trellis/spec/`, sync to `src/templates/markdown/spec/`
3. **Commit the spec updates** - This is the primary output, not just the analysis text
> **The analysis is worthless if it stays in chat. The value is in the updated specs.**

View File

@@ -0,0 +1,92 @@
---
name: trellis-check
description: "Comprehensive quality verification: spec compliance, lint, type-check, tests, cross-layer data flow, code reuse, and consistency checks. Use when code is written and needs quality verification, before committing changes, or to catch context drift during long sessions."
---
# Code Quality Check
Comprehensive quality verification for recently written code. Combines spec compliance, cross-layer safety, and pre-commit checks.
---
## Step 1: Identify What Changed
```bash
git diff --name-only HEAD
git status
```
## Step 2: Read Applicable Specs
```bash
python ./.trellis/scripts/get_context.py --mode packages
```
For each changed package/layer, read the spec index and follow its **Quality Check** section:
```bash
cat .trellis/spec/<package>/<layer>/index.md
```
Read the specific guideline files referenced — the index is a pointer, not the goal.
## Step 3: Run Project Checks
Run the project's lint, type-check, and test commands. Fix any failures before proceeding.
## Step 4: Review Against Checklist
### Code Quality
- [ ] Linter passes?
- [ ] Type checker passes (if applicable)?
- [ ] Tests pass?
- [ ] No debug logging left in?
- [ ] No suppressed warnings or type-safety bypasses?
### Test Coverage
- [ ] New function → unit test added?
- [ ] Bug fix → regression test added?
- [ ] Changed behavior → existing tests updated?
### Spec Sync
- [ ] Does `.trellis/spec/` need updates? (new patterns, conventions, lessons learned)
> "If I fixed a bug or discovered something non-obvious, should I document it so future me won't hit the same issue?" → If YES, update the relevant spec doc.
## Step 5: Cross-Layer Dimensions (if applicable)
Skip this step if your change is confined to a single layer.
### A. Data Flow (changes touch 3+ layers)
- [ ] Read flow traces correctly: Storage → Service → API → UI
- [ ] Write flow traces correctly: UI → API → Service → Storage
- [ ] Types/schemas correctly passed between layers?
- [ ] Errors properly propagated to caller?
### B. Code Reuse (modifying constants, creating utilities)
- [ ] Searched for existing similar code before creating new?
```bash
grep -r "pattern" src/
```
- [ ] If 2+ places define same value → extracted to shared constant?
- [ ] After batch modification, all occurrences updated?
### C. Import/Dependency (creating new files)
- [ ] Correct import paths (relative vs absolute)?
- [ ] No circular dependencies?
### D. Same-Layer Consistency
- [ ] Other places using the same concept are consistent?
---
## Step 6: Report and Fix
Report violations found and fix them directly. Re-run project checks after fixes.

View File

@@ -0,0 +1,73 @@
---
name: trellis-meta
description: "Understand and customize the local Trellis architecture inside a user project. Use when modifying .trellis plus platform hooks, settings, agents, skills, commands, prompts, or workflows generated by trellis init."
---
# Trellis Meta
This skill is for local Trellis users who have already run `trellis init` in a project. After reading it, an AI should understand the Trellis architecture, operating model, and customization entry points inside that user project, then modify the generated `.trellis/` and platform directory files according to the user's request.
The default operating scope is local files in the user project:
- `.trellis/`: workflow, config, tasks, spec, workspace, scripts, and runtime state.
- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories.
- Shared skill layer: `.agents/skills/`.
Do not assume the user has the Trellis source repository. Do not default to modifying the global npm install directory or `node_modules`.
## How To Use
1. Read `references/local-architecture/overview.md` first to establish the local Trellis system model.
2. If the request involves a specific AI tool, read `references/platform-files/platform-map.md` and the relevant platform file notes.
3. If the user wants to change behavior, read `references/customize-local/overview.md`, then open the specific customization topic.
4. Before editing, read the actual files in the user project and treat local content as authoritative.
## References
### Local Architecture
- `references/local-architecture/overview.md`: The three-layer local Trellis architecture and customization principles.
- `references/local-architecture/generated-files.md`: Files generated by `trellis init` and their customization boundaries.
- `references/local-architecture/workflow.md`: Phases, routing, and workflow-state blocks in `.trellis/workflow.md`.
- `references/local-architecture/task-system.md`: Task directories, active tasks, JSONL context, and task runtime.
- `references/local-architecture/spec-system.md`: How `.trellis/spec/` is organized and injected.
- `references/local-architecture/workspace-memory.md`: `.trellis/workspace/`, journals, and cross-session memory.
- `references/local-architecture/context-injection.md`: Hooks, sub-agent preludes, and context injection paths.
### Platform Files
- `references/platform-files/overview.md`: How shared `.trellis/` files relate to platform directories.
- `references/platform-files/platform-map.md`: Platform directories and paths for skills, agents, hooks, and extensions.
- `references/platform-files/hooks-and-settings.md`: How settings/config files, hooks, plugins, and extensions connect to Trellis.
- `references/platform-files/agents.md`: Local file responsibilities for `trellis-research`, `trellis-implement`, and `trellis-check`.
- `references/platform-files/skills-and-commands.md`: Differences between skills, commands, prompts, and workflows, plus how to change them.
### Local Customization
- `references/customize-local/overview.md`: Choose the right local customization entry point for the user's request.
- `references/customize-local/change-workflow.md`: Change phases, routing, next actions, and workflow-state.
- `references/customize-local/change-task-lifecycle.md`: Change task creation, status, archive behavior, and hooks.
- `references/customize-local/change-context-loading.md`: Change how tasks, specs, journals, and hook context are loaded.
- `references/customize-local/change-hooks.md`: Change platform hooks, settings, and shell session bridges.
- `references/customize-local/change-agents.md`: Change research, implement, and check agent behavior.
- `references/customize-local/change-skills-or-commands.md`: Add or modify local skills, commands, prompts, and workflows.
- `references/customize-local/change-spec-structure.md`: Adjust the project spec structure under `.trellis/spec/`.
- `references/customize-local/add-project-local-conventions.md`: Put team rules into project-local specs or local skills.
## Current Rules
- `.trellis/workflow.md` is the local workflow source of truth.
- `.trellis/config.yaml` is the project-level Trellis configuration and task hook configuration entry point.
- `.trellis/spec/` stores the user's project-specific coding conventions and design constraints.
- `.trellis/tasks/` stores task PRDs, technical notes, research files, and JSONL context.
- `.trellis/workspace/` stores developer journals and cross-session memory.
- Platform settings/config files decide which hooks, agents, skills, commands, prompts, and workflows actually run.
- `.trellis/.template-hashes.json` and `.trellis/.runtime/` are management/runtime state files. Confirm necessity before editing them.
## Do Not
- Do not treat Trellis upstream source code as the default target for local customization.
- Do not modify the global npm install directory or `node_modules/@mindfoldhq/trellis` to implement project needs.
- Do not overwrite user-modified local files with default templates.
- Do not put team-private project rules into the public `trellis-meta`; put project rules in `.trellis/spec/` or a project-local skill.
- Do not describe removed historical mechanisms as current Trellis behavior.

View File

@@ -0,0 +1,83 @@
# Add Project-Local Conventions
Often the user does not need to change Trellis mechanics; they need local AI to understand their team's conventions. In that case, prefer `.trellis/spec/` or a project-local skill instead of editing `trellis-meta`.
## Where To Put Things
| Content type | Location |
| --- | --- |
| Rules code must follow | `.trellis/spec/<layer>/` |
| Cross-layer thinking methods | `.trellis/spec/guides/` |
| AI capability for a project-specific flow | Platform-local skill |
| One-off task material | `.trellis/tasks/<task>/` |
| Session summary | `.trellis/workspace/<developer>/journal-N.md` |
## Create A Project-Local Skill
If the user wants AI to know "how this project customizes Trellis," create a local skill:
```text
.claude/skills/trellis-local/
└── SKILL.md
```
Example:
```md
---
name: trellis-local
description: "Project-local Trellis customizations for this repository. Use when changing this project's Trellis workflow, hooks, local agents, or team-specific conventions."
---
# Trellis Local
## Local Scope
This skill documents this repository's Trellis customizations only.
## Custom Workflow Rules
- ...
## Local Hook Changes
- ...
## Local Agent Changes
- ...
```
For multi-platform projects, place equivalent versions in other platform skill directories, or use `.agents/skills/` for platforms that support the shared layer.
## Write To `.trellis/spec/`
If the content is a coding convention, write it to spec. Examples:
```text
.trellis/spec/backend/error-handling.md
.trellis/spec/frontend/components.md
.trellis/spec/guides/cross-platform-thinking-guide.md
```
After writing it, update the corresponding `index.md` so AI can find the new rule from the entry point.
## Make The Current Task Use New Conventions
After writing a spec, add it to the current task context:
```bash
python ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/backend/error-handling.md" "Error handling conventions"
python ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/backend/error-handling.md" "Review error handling"
```
## Do Not Store Project-Private Rules In `trellis-meta`
`trellis-meta` is a public skill for understanding Trellis architecture and local customization entry points. Put project-private content in:
- `.trellis/spec/`
- a project-local skill
- the current task
- workspace journal
This prevents future updates to Trellis's built-in `trellis-meta` from overwriting the team's own conventions.

View File

@@ -0,0 +1,54 @@
# Change Local Agents
When the user wants to change `trellis-research`, `trellis-implement`, or `trellis-check` behavior, edit platform agent files in the user project.
## Read These Files First
1. Target platform agent directory
2. `.trellis/workflow.md` Phase 2 / research routing
3. Current task `prd.md`
4. Current task `implement.jsonl` / `check.jsonl`
5. Relevant hook or agent prelude
## Common Paths
| Platform | Path |
| --- | --- |
| Claude Code | `.claude/agents/trellis-*.md` |
| Cursor | `.cursor/agents/trellis-*.md` |
| OpenCode | `.opencode/agents/trellis-*.md` |
| Codex | `.codex/agents/trellis-*.toml` |
| Kiro | `.kiro/agents/trellis-*.json` |
| Gemini CLI | `.gemini/agents/trellis-*.md` |
| Qoder | `.qoder/agents/trellis-*.md` |
| CodeBuddy | `.codebuddy/agents/trellis-*.md` |
| Factory Droid | `.factory/droids/trellis-*.md` |
| Pi Agent | `.pi/agents/trellis-*.md` |
Use the actual paths in the user project as authoritative.
## Common Needs
| Need | Which agent to edit |
| --- | --- |
| Research must write files, not only reply in chat | `trellis-research` |
| Certain local specs must be read before implementation | `trellis-implement` + `implement.jsonl` configuration rules |
| Specific commands must run during checking | `trellis-check` |
| Agent must not modify certain directories | The corresponding agent's write boundary instructions |
| Agent output format must be fixed | The corresponding agent's final/reporting instructions |
## Modification Principles
1. **Preserve role boundaries**: research investigates and persists; implement writes implementation; check reviews and fixes.
2. **Do not hard-code project specs into agents**: long-term specs belong in `.trellis/spec/`; agents are responsible for reading them.
3. **Make read order explicit**: active task -> PRD -> info -> JSONL -> spec/research.
4. **Make write boundaries explicit**: which directories may be written and which may not.
5. **Synchronize across platforms**: when the user configured multiple platforms, decide whether to change only the current platform or all platform agents.
## Agent Pull Platforms
If an agent file contains a prelude for "read task/context after startup," do not remove those steps when editing. Otherwise the agent will work only from chat context and bypass Trellis's core mechanism.
## Hook Push Platforms
If context is injected by a hook, the agent file should still retain responsibility boundaries. Do not remove PRD/spec requirements from the agent just because a hook injects context.

View File

@@ -0,0 +1,81 @@
# Change Local Context Loading
Context loading determines when AI reads workflow, task, spec, research, workspace, and git status. Read this page when the user says "AI does not know the current task," "the agent did not read specs," or "there is too much/too little context."
## Read These Files First
1. `.trellis/workflow.md`
2. `.trellis/scripts/get_context.py`
3. `.trellis/scripts/common/session_context.py`
4. `.trellis/scripts/common/task_context.py`
5. `.trellis/scripts/common/active_task.py`
6. Current platform hooks or agent files
7. The current task's `implement.jsonl` / `check.jsonl`
## Context Sources
| Source | Purpose |
| --- | --- |
| `.trellis/workflow.md` | Workflow and next-action hints. |
| `.trellis/tasks/<task>/prd.md` | Current task requirements. |
| `.trellis/tasks/<task>/implement.jsonl` | Spec/research to read before implementation. |
| `.trellis/tasks/<task>/check.jsonl` | Spec/research to read during checking. |
| `.trellis/spec/` | Project specs. |
| `.trellis/workspace/` | Session records. |
| git status | Current working tree changes. |
## Common Needs And Edit Points
| Need | Edit point |
| --- | --- |
| Inject more/less information in new sessions | `session_context.py` or the platform `session-start` hook. |
| Change hints on each user input | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The `inject-workflow-state` hook is parser-only and reads the block verbatim. |
| Agent did not read specs | Task JSONL, agent prelude, `inject-subagent-context` hook. |
| Active task is lost | `active_task.py` and platform session identity propagation. |
| Change JSONL validation rules | `task_context.py`. |
## JSONL Rules
`implement.jsonl` / `check.jsonl` are the key context loading interface:
```jsonl
{"file": ".trellis/spec/backend/index.md", "reason": "Backend conventions"}
{"file": ".trellis/tasks/04-28-x/research/api.md", "reason": "API research"}
```
Include only spec/research files. Do not put code files that will be modified into these manifests; agents read code files themselves during implementation.
## Change Session Context
If the user wants every new session to see more project state, edit:
- `.trellis/scripts/common/session_context.py`
- the corresponding platform `session-start` hook
Context cannot grow without bound. Prefer injecting indexes and paths so the AI can read detailed files on demand.
## Change Sub-Agent Context
First determine which mode the platform uses:
- hook push: edit the `inject-subagent-context` hook.
- agent pull: edit the read steps in the corresponding `trellis-implement` / `trellis-check` agent file.
In both modes, make sure the agent ultimately reads:
1. active task
2. `prd.md`
3. `info.md` if present
4. the corresponding JSONL
5. spec/research referenced by the JSONL
## Troubleshooting Order
```bash
python ./.trellis/scripts/task.py current --source
python ./.trellis/scripts/task.py list-context <task>
python ./.trellis/scripts/task.py validate <task>
python ./.trellis/scripts/get_context.py --mode packages
```
Confirm the task and JSONL are correct before editing hooks/agents.

View File

@@ -0,0 +1,57 @@
# Change Local Hooks
Hooks are the automation layer that connects a platform to Trellis. When the user wants to change "when context is injected," "how shell commands inherit a session," or "which files are read before an agent starts," hooks are usually the edit point.
## Read These Files First
1. Target platform settings/config, such as `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`
2. Target platform hooks directory
3. `.trellis/scripts/common/active_task.py`
4. `.trellis/scripts/common/session_context.py`
5. `.trellis/workflow.md`
## Common Hook Types
| Hook | Purpose |
| --- | --- |
| session-start | Injects a Trellis overview when a session starts, clears, or compacts. |
| workflow-state | Injects a state hint on each user input. |
| sub-agent context | Injects PRD/spec/research before an agent starts. |
| shell session bridge | Lets `task.py` commands in shell see the same session identity. |
## Modification Steps
1. Find the hook registration in settings/config.
2. Confirm the registered script path exists.
3. Read the hook script and identify inputs, outputs, and called `.trellis/scripts/`.
4. Modify hook behavior.
5. If the hook depends on workflow content, synchronize `.trellis/workflow.md`.
## Example: Change New-Session Injection Content
First find the session-start hook:
```text
.claude/settings.json
.claude/hooks/session-start.py
```
If the hook ultimately calls `.trellis/scripts/get_context.py` or `session_context.py`, editing the local script is usually more robust than hard-coding content in the hook.
## Example: Agent Did Not Read JSONL
First confirm:
```bash
python ./.trellis/scripts/task.py current --source
python ./.trellis/scripts/task.py validate <task>
```
If the task and JSONL are correct, determine whether the platform uses hook push or agent pull. For hook push, edit `inject-subagent-context`; for agent pull, edit the agent file.
## Notes
- Settings handle registration, hook scripts handle behavior; inspect both together.
- Different platforms support different hook events. Do not directly copy another platform's settings.
- Hooks should read project-local `.trellis/`; they should not depend on Trellis upstream source paths.
- Hook failures should produce visible errors so AI does not silently lose context.

View File

@@ -0,0 +1,78 @@
# Change Local Skills, Commands, Prompts, And Workflows
When the user wants to change AI entry points, auto-trigger rules, or explicit command behavior, edit skills, commands, prompts, or workflows in local platform directories.
## Read These Files First
1. `.trellis/workflow.md`
2. Target platform skill/command/prompt/workflow directory
3. Related agent or hook files
4. Whether project rules already exist in `.trellis/spec/`
## Which Entry Type To Choose
| Goal | Recommendation |
| --- | --- |
| AI should automatically know a capability | Add or modify a skill. |
| User wants to trigger manually with a command | Add or modify a command/prompt/workflow. |
| Team project conventions | Prefer `.trellis/spec/` or a project-local skill. |
| Change Trellis flow semantics | Synchronize `.trellis/workflow.md`. |
## Modify A Skill
A skill is usually:
```text
<skill-name>/
├── SKILL.md
└── references/
```
`SKILL.md` should be short and responsible for triggering/routing. Put long content in `references/` so AI can read it on demand.
The frontmatter description should specify when to use the skill. Example:
```yaml
description: "Use when customizing this project's deployment workflow and release checklist."
```
Do not write vague descriptions such as "helpful project skill"; they can trigger incorrectly.
## Modify A Command/Prompt/Workflow
Explicit entry points should state:
- How the user triggers it.
- Which `.trellis/` files to read.
- Which scripts to run.
- How to report after completion.
If a command only repeats workflow rules, prefer making it reference/read `.trellis/workflow.md` instead of maintaining a second copy of the flow.
## Common Paths
| Platform | Entry directories |
| --- | --- |
| Claude Code | `.claude/skills/`, `.claude/commands/` |
| Cursor | `.cursor/skills/`, `.cursor/commands/` |
| OpenCode | `.opencode/skills/`, `.opencode/commands/` |
| Codex | `.agents/skills/`, `.codex/skills/` |
| GitHub Copilot | `.github/skills/`, `.github/prompts/` |
| Kilo / Antigravity / Windsurf | workflows + skills |
## Add A Project-Local Skill
If the user wants to document team-private customizations, create a project-local skill, for example:
```text
.claude/skills/project-trellis-local/
└── SKILL.md
```
For multi-platform projects, add equivalent versions in each platform skill directory, or use `.agents/skills/` on platforms that support the shared layer.
## Notes
- Do not mix every platform's syntax into one file.
- Do not change only one platform entry point while claiming all platforms are supported.
- Do not hide long-term engineering conventions inside a command; write them to `.trellis/spec/`.

View File

@@ -0,0 +1,83 @@
# Change Local Spec Structure
When the user wants to change the engineering conventions AI follows, add new spec layers, or adjust monorepo package mapping, edit `.trellis/spec/` and `.trellis/config.yaml`.
## Read These Files First
1. `.trellis/config.yaml`
2. `.trellis/spec/`
3. `.trellis/workflow.md` Phase 1.3 and Phase 3.3
4. Current task `implement.jsonl` / `check.jsonl`
## Common Needs
| Need | Edit location |
| --- | --- |
| Add backend/frontend/docs/test spec layer | `.trellis/spec/<layer>/` or `.trellis/spec/<package>/<layer>/` |
| Add shared thinking guides | `.trellis/spec/guides/` |
| Adjust monorepo packages | `packages` in `.trellis/config.yaml` |
| Change default package | `default_package` in `.trellis/config.yaml` |
| Control spec scanning scope | `spec_scope` in `.trellis/config.yaml` |
| Make a task read a new spec | Task `implement.jsonl` / `check.jsonl` |
## Add A Spec Layer
Single-repository example:
```text
.trellis/spec/security/
├── index.md
└── auth.md
```
Monorepo example:
```text
.trellis/spec/webapp/security/
├── index.md
└── auth.md
```
`index.md` should include:
- What code this layer applies to.
- Pre-Development Checklist.
- Quality Check.
- Links to specific guideline files.
## Update Context
Adding a spec does not mean every task automatically reads it. The current task must reference it in JSONL:
```bash
python ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/webapp/security/index.md" "Security conventions"
python ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/webapp/security/index.md" "Security review rules"
```
## Change Monorepo Packages
Example `.trellis/config.yaml`:
```yaml
packages:
webapp:
path: apps/web
api:
path: apps/api
default_package: webapp
```
After editing, run:
```bash
python ./.trellis/scripts/get_context.py --mode packages
```
Use this output to confirm AI can see the correct packages and spec layers.
## Notes
- Specs are user project conventions and can be changed according to project needs.
- Do not put temporary task information into specs; put temporary information in the task.
- Do not put long-term conventions only in agents or commands; preserve them in specs.
- After changing spec structure, check whether existing task JSONL files still point to files that exist.

View File

@@ -0,0 +1,90 @@
# Change Local Task Lifecycle
Task lifecycle includes creation, start, context configuration, finish, archive, parent/child tasks, and lifecycle hooks. The default customization targets are `.trellis/tasks/`, `.trellis/config.yaml`, and `.trellis/scripts/`.
## Read These Files First
1. `.trellis/workflow.md`
2. `.trellis/config.yaml`
3. `.trellis/scripts/task.py`
4. `.trellis/scripts/common/task_store.py`
5. `.trellis/scripts/common/task_utils.py`
6. The current task's `.trellis/tasks/<task>/task.json`
## Common Needs And Edit Points
| Need | Edit point |
| --- | --- |
| Automatically sync an external system after task creation | `hooks.after_create` in `.trellis/config.yaml`. |
| Automatically update status after task start | `hooks.after_start` in `.trellis/config.yaml`. |
| Run a script after task finish | `hooks.after_finish` in `.trellis/config.yaml`. |
| Clean external resources after archive | `hooks.after_archive` in `.trellis/config.yaml`. |
| Change default task fields | `.trellis/scripts/common/task_store.py`. |
| Change task parsing/search | `.trellis/scripts/common/task_utils.py`. |
| Change active task behavior | `.trellis/scripts/common/active_task.py`. |
## lifecycle hooks
`.trellis/config.yaml` supports:
```yaml
hooks:
after_create:
- "python .trellis/scripts/hooks/my_sync.py create"
after_start:
- "python .trellis/scripts/hooks/my_sync.py start"
after_finish:
- "python .trellis/scripts/hooks/my_sync.py finish"
after_archive:
- "python .trellis/scripts/hooks/my_sync.py archive"
```
Hook commands receive the `TASK_JSON_PATH` environment variable, pointing to the current task's `task.json`. Hook failures should usually warn, but not block the main task operation.
## Change Task Fields
If the user wants to add project-local fields, prefer putting them under `meta` in `task.json` to avoid breaking existing scripts' assumptions about standard fields.
Example:
```json
"meta": {
"linearIssue": "ENG-123",
"risk": "high"
}
```
If standard fields really need to change, inspect every local script that reads `task.json`.
## Change Active Task
Active task is session-level state stored in `.trellis/.runtime/sessions/`. Do not fall back to a global `.current-task` model. If the user wants to change active task behavior, edit:
- `.trellis/scripts/common/active_task.py`
- platform hooks or shell session bridges
- active task descriptions in `.trellis/workflow.md`
### `task.py create` Sets the Active Pointer
`cmd_create` in `.trellis/scripts/common/task_store.py` calls `set_active_task` best-effort right after writing the new task directory. The behavior:
- When the calling shell carries session identity (`TRELLIS_CONTEXT_ID` env var, or any platform-specific session env that `resolve_context_key` recognizes — see `active_task.py:_ENV_SESSION_KEYS`), the per-session pointer at `.trellis/.runtime/sessions/<context_key>.json` is rewritten to point at the new task. The task's `status=planning` and `[workflow-state:planning]` fires on the very next `UserPromptSubmit`.
- When session identity is unavailable (raw CLI invocation outside an AI session, or a platform that doesn't propagate identity to shell), the task directory is still created and `status=planning` is still written, but the active pointer is left untouched. The user can attach the task later with `task.py start <dir>` once they're back in an AI session.
This makes `[workflow-state:planning]` the live breadcrumb during the brainstorm and JSONL curation work that follows `task.py create`. The pre-R7 behavior left the breadcrumb stuck on `no_task` until `task.py start`, so the planning block was effectively dead text.
If you fork `task.py` to add a new creation path (e.g. an external import that bypasses `cmd_create`), audit whether your path also calls `set_active_task`. Without that call, your created tasks will not surface as active. The full status writer table is in `.trellis/spec/cli/backend/workflow-state-contract.md`.
## Modification Steps
1. Confirm the current task with `python ./.trellis/scripts/task.py current --source`.
2. Read the current task's `task.json` and confirm status and fields.
3. For configuration needs, edit `.trellis/config.yaml` first.
4. For script behavior needs, then edit `.trellis/scripts/`.
5. If the AI flow changed, synchronize `.trellis/workflow.md`.
## Do Not
- Do not directly edit `.trellis/.runtime/sessions/` to "fix" business state.
- Do not hard-code project-private fields into scripts; prefer `meta`.
- Do not default to asking the user to fork Trellis CLI.

View File

@@ -0,0 +1,64 @@
# Change Local Workflow
When the user wants to change Trellis phases, next-action hints, whether to create tasks, whether to use sub-agents, or when to check/wrap up, edit `.trellis/workflow.md` first.
## Read These Files First
1. `.trellis/workflow.md`
2. Entry files for the current platform, such as skills/commands/prompts/workflows
3. The current task's `task.json` and `prd.md`
## Common Needs And Edit Points
| Need | Edit point |
| --- | --- |
| Change phase names or phase order | `Phase Index` and the corresponding Phase sections. |
| Change whether to create a task when there is no task | `[workflow-state:no_task]` state block. |
| Change the next step during planning | Phase 1 and `[workflow-state:planning]`. |
| Change whether an agent is required during in_progress | Phase 2 and `[workflow-state:in_progress]`. |
| Change wrap-up after completion | Phase 3 and `[workflow-state:completed]`. |
| Change which skill a user intent triggers | `Skill Routing` table. |
## Modification Steps
1. Find the relevant section in `.trellis/workflow.md`.
2. When changing rules, keep explicit trigger conditions and next actions.
3. If adding or renaming a skill/agent, synchronize the corresponding files in platform directories.
4. Workflow-state changes only need an edit to the `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook is parser-only — it reads whatever you put in the block. Keep the opening and closing tags' STATUS strings identical (`[workflow-state:foo]…[/workflow-state:foo]`); mismatched STATUS pairs are silently dropped.
5. Make the AI reread `.trellis/workflow.md`; do not keep using rules from the old conversation.
## Example: Relax Task Creation Requirements
To change when task creation can be skipped, usually edit `[workflow-state:no_task]`:
```md
[workflow-state:no_task]
Task is not required when the answer is a one-reply explanation, no files are changed, and no research is needed.
[/workflow-state:no_task]
```
If the formal Phase 1 flow also needs to change, synchronize the Phase 1 section.
## Example: One Platform Does Not Use Sub-Agents
If the user wants only one platform to avoid sub-agents, first confirm whether that platform has a separate group in the workflow. Then change Phase 2 routing for that platform group instead of deleting all `trellis-implement` / `trellis-check` instructions across platforms.
## `/trellis:continue` Route Table
`/trellis:continue` resumes a task by deciding which phase step to load next. The decision combines `task.json.status` with the presence of artifacts inside the task directory. The mapping is fixed in the command itself; forks that add custom statuses must extend both the workflow.md tag block and this table.
| `status` | Artifact state | Resume at |
| --- | --- | --- |
| `planning` | `prd.md` missing | Phase 1.1 (load `trellis-brainstorm`) |
| `planning` | `prd.md` exists, `implement.jsonl` only has the seed `_example` row | Phase 1.3 (curate JSONL context) |
| `planning` | `prd.md` exists, `implement.jsonl` curated | Phase 1.4 (run `task.py start`) |
| `in_progress` | no implementation in conversation history | Phase 2.1 (`trellis-implement`) |
| `in_progress` | implementation done, no `trellis-check` run | Phase 2.2 (`trellis-check`) |
| `in_progress` | check passed | Phase 3.1 (verify quality + spec update) |
| `completed` | task is still in active tree | Phase 3.5 (run `/trellis:finish-work` to archive) |
When you add a custom status (e.g. `in-review`), add a `[workflow-state:in-review]` block in `.trellis/workflow.md` for the per-turn breadcrumb AND extend this route table — usually by editing the `/trellis:continue` command file (`.{platform}/commands/trellis/continue.md` or equivalent) to add a row that decides where to resume from. Without the route entry, `/trellis:continue` will fall through to a default branch and the user will not land on the step you intended.
## Notes
`.trellis/workflow.md` is the local project workflow, not an immutable template. The user can adapt it to team habits. After editing it, platform entry files may still contain old descriptions, so inspect them too.

View File

@@ -0,0 +1,55 @@
# Local Customization Overview
This directory is for local AI working in a user project where Trellis was installed through npm and `trellis init` has already been run. The AI should modify generated `.trellis/` and platform directories inside the project, not Trellis CLI upstream source code.
## First Determine What The User Actually Wants To Change
| User wording | Read first |
| --- | --- |
| "Change the Trellis flow / phases / next prompt" | `change-workflow.md` |
| "Change task creation, status, archive, or hooks" | `change-task-lifecycle.md` |
| "AI did not read context / change injected content" | `change-context-loading.md` |
| "A platform hook is not behaving as expected" | `change-hooks.md` |
| "Change implement/check/research agent behavior" | `change-agents.md` |
| "Add a skill/command/workflow/prompt" | `change-skills-or-commands.md` |
| "Adjust the project spec structure" | `change-spec-structure.md` |
| "Add team conventions and local notes" | `add-project-local-conventions.md` |
## General Operation Order
1. **Confirm platform and directories**: inspect which directories exist, such as `.claude/`, `.codex/`, `.cursor/`.
2. **Confirm the current active task**: run `python ./.trellis/scripts/task.py current --source`.
3. **Read the local source of truth**: prefer `.trellis/workflow.md`, `.trellis/config.yaml`, and relevant platform files.
4. **Modify narrowly**: edit only files related to the user's request.
5. **Synchronize semantics**: if a shared flow changes, check whether platform entry points also need changes; if a platform entry changes, check whether `.trellis/workflow.md` still agrees.
## Local File Priority
| Layer | Files |
| --- | --- |
| Workflow | `.trellis/workflow.md` |
| Project configuration | `.trellis/config.yaml` |
| Task material | `.trellis/tasks/<task>/` |
| Project specs | `.trellis/spec/` |
| Runtime scripts | `.trellis/scripts/` |
| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, and similar directories |
| Shared skill | `.agents/skills/` |
## Things Not To Do By Default
- Do not edit the global npm install directory.
- Do not edit `node_modules/@mindfoldhq/trellis`.
- Do not assume the user has the Trellis GitHub repository.
- Do not overwrite local files already modified by the user with default templates.
- Do not put team project rules into public `trellis-meta`; project rules belong in `.trellis/spec/` or a local skill.
## When To Inspect Upstream Source
Switch to an upstream source-code perspective only when the user explicitly expresses one of these goals:
- "I want to open a PR to Trellis"
- "I want to change npm package publish contents"
- "I want to fork Trellis"
- "I want to modify the generation logic for `trellis init/update`"
Otherwise, default to modifying local Trellis files inside the user project.

View File

@@ -0,0 +1,68 @@
# Local Context Injection System
Trellis context injection aims to make AI read the right files at the right time instead of relying on model memory. In a user project, injection is implemented by `.trellis/` scripts together with platform hooks, agents, and skills.
## Injected Context Types
| Type | Source | Purpose |
| --- | --- | --- |
| session context | `.trellis/scripts/get_context.py` | Current developer, git status, active task, active tasks, journal, packages. |
| workflow context | `.trellis/workflow.md` | Current Trellis flow and next action. |
| spec context | `.trellis/spec/` + task JSONL | Specs that must be followed during implementation/checking. |
| task context | `.trellis/tasks/<task>/prd.md`, `info.md`, `research/` | Current task requirements, design, and research. |
| platform context | Platform hooks/settings/agents | Lets different AI tools read the files above through their own mechanisms. |
## session-start
Platforms with session-start support inject a Trellis overview when a session starts, clears, compacts, or receives a similar event. Injected content usually includes:
- workflow summary.
- current task status.
- active tasks.
- spec index paths.
- developer identity and git status.
If the user feels the AI does not know the current task in a new session, first check whether the platform's session-start hook or equivalent mechanism is installed and running.
## workflow-state
workflow-state is a lightweight hint injected around each user turn. Based on current task status, it selects a block from `.trellis/workflow.md`, such as `no_task`, `planning`, `in_progress`, or `completed`.
If the user wants to change "what the AI should do next in a given state," edit the corresponding state block in `.trellis/workflow.md` first.
## sub-agent context
Implement and check agents need task context. Trellis has two loading modes:
1. **hook push**: a platform hook injects `prd.md` and the files referenced by `implement.jsonl` / `check.jsonl` before the agent starts.
2. **agent pull**: the agent definition instructs the agent to read the active task, PRD, and JSONL context after startup.
In both modes, JSONL files in the task directory are the key interface.
## JSONL Reading Rules
`implement.jsonl` and `check.jsonl` contain one JSON object per line:
```jsonl
{"file": ".trellis/spec/backend/index.md", "reason": "Backend rules"}
```
Readers should skip seed rows without a `file` field. When configuring JSONL, the AI should include only spec/research files, not pre-register code files that will be modified.
## Active Task And Context Key
Active task state lives in `.trellis/.runtime/sessions/` and is isolated per session. Hooks try to resolve the context key from platform events, environment variables, transcript paths, or `TRELLIS_CONTEXT_ID`.
If shell commands cannot see the same context key, `task.py current --source` may report no active task. In that case, check whether the platform passes session identity into the shell instead of hand-writing a global current-task file.
## Local Customization Points
| Need | Edit location |
| --- | --- |
| Change session-start injected content | The platform's `session-start` hook or plugin file. |
| Change per-turn workflow-state rules | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The platform workflow-state hook parses these blocks verbatim and embeds no fallback text. |
| Change how sub-agents read context | Platform agent definitions, the `inject-subagent-context` hook, or agent preludes. |
| Change JSONL validation/display | `.trellis/scripts/common/task_context.py`. |
| Change active task resolution | `.trellis/scripts/common/active_task.py`. |
When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct PRD/spec/research.

View File

@@ -0,0 +1,80 @@
# Local Files Generated After Init
`trellis init` writes the Trellis runtime into the user project. Later, `trellis update` tries to update Trellis-managed template files, but it uses `.trellis/.template-hashes.json` to determine which files have already been modified by the user.
This page only describes files that are visible and editable inside the user project.
## `.trellis/`
```text
.trellis/
├── workflow.md
├── config.yaml
├── .developer
├── .version
├── .template-hashes.json
├── .runtime/
├── scripts/
├── spec/
├── tasks/
└── workspace/
```
| Path | Usually editable? | Notes |
| --- | --- | --- |
| `.trellis/workflow.md` | Yes | Local workflow documentation and AI routing rules. |
| `.trellis/config.yaml` | Yes | Project configuration, hooks, packages, journal line limits, and related settings. |
| `.trellis/spec/` | Yes | Project specs, intended to be updated regularly by users and AI. |
| `.trellis/tasks/` | Yes | Task material and research artifacts, maintained by the task workflow. |
| `.trellis/workspace/` | Yes | Session records, usually written by `add_session.py`. |
| `.trellis/scripts/` | Carefully | Local runtime. It can be customized, but only after understanding the call chain. |
| `.trellis/.runtime/` | No | Runtime state, usually written automatically by hooks/scripts. |
| `.trellis/.developer` | Carefully | Current developer identity. |
| `.trellis/.version` | No | Trellis version record used by update/migration logic. |
| `.trellis/.template-hashes.json` | No | Template hash record. Do not hand-write business rules here. |
## Platform Directories
Different platforms generate different directories. Common categories:
| Category | Example paths | Purpose |
| --- | --- | --- |
| hooks | `.claude/hooks/`, `.codex/hooks/`, `.cursor/hooks/` | Inject session context, workflow-state, and sub-agent context. |
| settings | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json` | Tell the platform when to run hooks or plugins. |
| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/` | Define agents such as `trellis-research`, `trellis-implement`, and `trellis-check`. |
| skills | `.claude/skills/`, `.agents/skills/`, `.qoder/skills/` | Skills that auto-trigger or can be read by AI. |
| commands/prompts/workflows | `.cursor/commands/`, `.github/prompts/`, `.windsurf/workflows/` | Explicit user-invoked command or workflow entry points. |
When modifying a platform directory, also confirm whether `.trellis/workflow.md` still describes the same flow.
## Meaning Of Template Hashes
`.trellis/.template-hashes.json` records the content hash from the last time Trellis wrote a template file. `trellis update` uses it to distinguish three cases:
| Case | Update behavior |
| --- | --- |
| File was not modified by the user | It can be updated automatically. |
| File was modified by the user | Prompt the user to overwrite, keep, or generate `.new`. |
| File is no longer a current template | It may be deleted, renamed, or preserved according to migration rules. |
When an AI customizes local Trellis files, it does not need to maintain hashes manually. It is normal for Trellis update to recognize the result as "modified by the user."
## Local Customization Boundaries
Editable by default:
- `.trellis/workflow.md`
- `.trellis/config.yaml`
- `.trellis/spec/**`
- `.trellis/scripts/**`
- Platform hooks, settings, agents, skills, commands, prompts, and workflows
Do not edit by default:
- Global npm install directory
- `node_modules/@mindfoldhq/trellis`
- Trellis GitHub repository source code
- Concrete state files under `.trellis/.runtime/**`
- Hash contents inside `.trellis/.template-hashes.json`
Switch to the Trellis CLI source-code perspective only when the user explicitly wants to contribute upstream.

View File

@@ -0,0 +1,51 @@
# Local Trellis Architecture Overview
`trellis-meta` is for user projects that have already run `trellis init`. The user's machine usually has only the npm-installed `trellis` command plus the Trellis files generated inside the project; it may not have the Trellis CLI source code.
Therefore, when an AI uses this skill, the default customization target is local files inside the user project:
- `.trellis/`: workflow, tasks, specs, memory, scripts, and runtime state.
- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories.
- Shared skill layer: `.agents/skills/`.
Do not default to guiding the user to fork the Trellis CLI repository. Treat upstream source code as the operating target only when the user explicitly says they want to change Trellis upstream source, publish an npm package, or contribute a PR.
## Local System Model
Trellis provides three layers inside a user project:
1. **Workflow layer**: `.trellis/workflow.md` defines phases, routing, next actions, and prompt blocks.
2. **Persistence layer**: `.trellis/tasks/`, `.trellis/spec/`, and `.trellis/workspace/` store tasks, specs, and session memory.
3. **Platform integration layer**: hooks, settings, agents, skills, commands, prompts, and workflows in platform directories connect the Trellis workflow to different AI tools.
All three layers live inside the user project, so an AI can read and modify them directly.
## Core Paths
| Path | Purpose |
| --- | --- |
| `.trellis/workflow.md` | Workflow phases, skill routing, and workflow-state prompt blocks. |
| `.trellis/config.yaml` | Project configuration, task lifecycle hooks, monorepo package configuration, and journal configuration. |
| `.trellis/spec/` | The user's project-specific coding conventions and thinking guides. |
| `.trellis/tasks/` | Each task's PRD, technical notes, research files, and JSONL context. |
| `.trellis/workspace/` | Per-developer journals and cross-session memory. |
| `.trellis/scripts/` | Local Python runtime used by commands, hooks, and context injection. |
| `.trellis/.runtime/` | Session-level runtime state, such as the current task pointer. |
| `.trellis/.template-hashes.json` | Template hashes for Trellis-managed files, used by update to determine whether local files were modified by the user. |
## AI Customization Principles
1. **Find the local source of truth first**: Do not edit from memory. Read `.trellis/workflow.md`, `.trellis/config.yaml`, the relevant platform directory, and related task files first.
2. **Edit the user project, not the npm package cache**: Modify generated files inside the project, not `node_modules` or the global npm install directory.
3. **Keep platform files aligned with `.trellis/`**: If workflow routing changes, also check whether platform skills or commands still describe the same flow.
4. **Put project-specific rules in `.trellis/spec/` or a local skill**: Do not put team conventions into `trellis-meta`.
5. **Preserve user changes**: If a file was already modified locally, work from the current content instead of overwriting it with a default template.
## How To Use This Directory
- To understand which files exist after init, read `generated-files.md`.
- To change phases, routing, or next actions, read `workflow.md`.
- To change the task model, JSONL context, or active task behavior, read `task-system.md`.
- To change coding convention injection, read `spec-system.md`.
- To understand journals and cross-session memory, read `workspace-memory.md`.
- To change hooks or sub-agent context loading, read `context-injection.md`.

View File

@@ -0,0 +1,102 @@
# Local Spec System
`.trellis/spec/` is the user's project-specific engineering spec library. Trellis is not about making AI memorize conventions; it injects relevant specs or requires the AI to read them at the right time.
## Directory Model
A common single-repository structure:
```text
.trellis/spec/
├── backend/
│ ├── index.md
│ └── ...
├── frontend/
│ ├── index.md
│ └── ...
└── guides/
├── index.md
└── ...
```
A common monorepo structure:
```text
.trellis/spec/
├── cli/
│ ├── backend/
│ │ ├── index.md
│ │ └── ...
│ └── unit-test/
│ ├── index.md
│ └── ...
├── docs-site/
│ └── docs/
│ ├── index.md
│ └── ...
└── guides/
├── index.md
└── ...
```
`index.md` is the entry point for each layer. It should list the Pre-Development Checklist and Quality Check. Specific guidelines live in other Markdown files in the same directory.
## Package Configuration
`.trellis/config.yaml` can declare packages:
```yaml
packages:
cli:
path: packages/cli
docs-site:
path: docs-site
type: submodule
default_package: cli
```
The AI can run:
```bash
python ./.trellis/scripts/get_context.py --mode packages
```
This command lists packages and spec layers for the current project. Use this output as the reference when configuring context JSONL.
## How Specs Enter Tasks
Before a task enters implementation, Phase 1.3 should write relevant specs into `implement.jsonl` / `check.jsonl`:
```jsonl
{"file": ".trellis/spec/cli/backend/index.md", "reason": "CLI backend conventions"}
{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Test expectations"}
```
Sub-agents or platform preludes read these JSONL files and load the referenced specs. On platforms without sub-agent support, the AI should read the relevant specs directly according to the workflow.
## What Specs Should Contain
Specs should contain executable engineering conventions for the project, not generic best practices:
- Where files should live.
- How error handling should be expressed.
- Input/output contracts for APIs, hooks, and commands.
- Patterns that are forbidden.
- Cases that require tests.
- Project-specific pitfalls and how to avoid them.
When the AI learns a new rule during implementation or debugging, it should update `.trellis/spec/` rather than only summarizing it in chat.
## Local Customization Points
| Need | Edit location |
| --- | --- |
| Add a new spec layer | `.trellis/spec/<package>/<layer>/index.md` and corresponding guideline files. |
| Change monorepo spec mapping | `packages` / `default_package` / `spec_scope` in `.trellis/config.yaml`. |
| Change which specs AI reads before implementation | The task's `implement.jsonl`. |
| Change which specs AI reads during checking | The task's `check.jsonl`. |
| Change when specs should be updated | Phase 3.3 in `.trellis/workflow.md` and the `trellis-update-spec` skill. |
## Boundaries
`.trellis/spec/` is the user's project specification, not a permanent copy of Trellis built-in templates. The AI should encourage the user to update it according to the actual project code instead of treating Trellis default templates as immutable documents.

View File

@@ -0,0 +1,101 @@
# Local Task System
The Trellis task system is stored entirely under `.trellis/tasks/` in the user project. Each task is a directory containing requirements, context, research, state, and relationship information.
## Task Directory Structure
```text
.trellis/tasks/
├── 04-28-example-task/
│ ├── task.json
│ ├── prd.md
│ ├── info.md
│ ├── implement.jsonl
│ ├── check.jsonl
│ └── research/
└── archive/
└── 2026-04/
```
| File | Purpose |
| --- | --- |
| `task.json` | Task metadata: status, assignee, priority, branch, parent/child tasks, and similar fields. |
| `prd.md` | Requirements document; the most important business context during implementation. |
| `info.md` | Optional technical design. |
| `implement.jsonl` | List of spec/research files the implement agent must read first. |
| `check.jsonl` | List of spec/research files the check agent must read first. |
| `research/` | Research artifacts. Complex findings should not live only in chat. |
## `task.json`
`task.json` records task status and metadata. Common fields:
| Field | Meaning |
| --- | --- |
| `id` / `name` / `title` | Task identity and title. |
| `status` | Status such as `planning`, `in_progress`, `review`, or `completed`. |
| `priority` | `P0`, `P1`, `P2`, `P3`. |
| `creator` / `assignee` | Creator and assignee. |
| `package` | Target package in a monorepo; may be empty. |
| `branch` / `base_branch` | Working branch and PR target branch. |
| `children` / `parent` | Parent/child task relationships. |
| `commit` / `pr_url` | Commit and PR information after completion. |
| `meta` | Extension fields. |
The AI should not treat phase numbers as task status. Task progress is mainly determined by `status`, `prd.md`, whether JSONL context is configured, and the phase descriptions in `workflow.md`.
## Active Task
The user sees a "current task," but Trellis stores active task state per session.
```text
.trellis/.runtime/sessions/<context-key>.json
```
`task.py start` writes the task path into the runtime session file for the current session. `task.py current --source` shows the current task and where it came from. Different AI windows can point to different tasks without overwriting each other.
If the platform or shell environment has no stable session identity, `task.py start` may be unable to set the active task. The AI should read the error, inspect the platform hook/session environment, and not fall back to a shared global pointer.
## JSONL Context
`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first.
Format:
```jsonl
{"file": ".trellis/spec/cli/backend/index.md", "reason": "Backend conventions"}
{"file": ".trellis/tasks/04-28-example/research/api.md", "reason": "API research"}
```
Rules:
- Include spec and research files.
- Do not include code files that are about to be modified.
- Do not treat temporary conclusions in chat as the only context.
- Seed rows have no `file` field; they only prompt the AI to fill in real entries.
## Common Commands
```bash
python ./.trellis/scripts/task.py create "<title>" --slug <slug>
python ./.trellis/scripts/task.py start <task>
python ./.trellis/scripts/task.py current --source
python ./.trellis/scripts/task.py add-context <task> implement <file> <reason>
python ./.trellis/scripts/task.py validate <task>
python ./.trellis/scripts/task.py finish
python ./.trellis/scripts/task.py archive <task>
```
When modifying the task system, the AI should prefer script commands to maintain structure. Edit JSON/Markdown directly only when scripts do not cover the need.
## Local Customization Points
| Need | Edit location |
| --- | --- |
| Change the default task template | `.trellis/scripts/common/task_store.py` and task creation instructions. |
| Change status semantics | `.trellis/workflow.md`, workflow-state hook logic, and task usage conventions. |
| Add task lifecycle actions | `hooks.after_*` in `.trellis/config.yaml`. |
| Change context rules | Phase 1.3 in `.trellis/workflow.md` and related platform agent/hook instructions. |
| Change archive policy | `.trellis/scripts/common/task_store.py` / `task_utils.py`. |
These are local files in the user project. Do not default to editing Trellis CLI source code unless the user wants to contribute upstream.

View File

@@ -0,0 +1,75 @@
# Local Workflow System
`.trellis/workflow.md` is the Trellis workflow source of truth inside the user project. An AI does not need Trellis source code to understand how the current project should move tasks forward; this file is enough.
## File Responsibilities
`.trellis/workflow.md` has three responsibilities:
1. **Explain workflow phases**: Plan, Execute, Finish.
2. **Define skill routing**: which skill or agent the AI should use when the user expresses a certain intent.
3. **Provide workflow-state prompt blocks**: hooks can inject the prompt block for the current state into the conversation.
## Current Phase Model
```text
Phase 1: Plan -> clarify what to build, produce prd.md and required research
Phase 2: Execute -> implement against the PRD and specs, then check
Phase 3: Finish -> final verification, preserve lessons, and wrap up
```
Each phase contains numbered steps, such as `1.3 Configure context`. These numbers are not runtime fields in `task.json`; they are workflow structure for AI and humans to read.
## Skill Routing
`workflow.md` separates routing by platform capability:
- Platforms with sub-agent support: dispatch `trellis-implement` by default for implementation and `trellis-check` for checking.
- Platforms without sub-agent support: the main session reads skills such as `trellis-before-dev`, then executes directly.
When changing local AI behavior, update the routing descriptions in `workflow.md` first, then check whether the corresponding platform skill, command, or agent files need to stay in sync.
## Workflow-State Prompt Blocks
The bottom of `workflow.md` can contain state blocks like this:
```text
[workflow-state:no_task]
...
[/workflow-state:no_task]
```
Hooks choose the right block based on current task status and inject it into the conversation. Common states include:
| State | Meaning |
| --- | --- |
| `no_task` | The current session has no active task. |
| `planning` | The task is still in requirements, research, or context configuration. |
| `in_progress` | The task has entered implementation and checking. |
| `completed` | The task is complete and waiting for wrap-up or archive. |
If the user wants to change policies such as "whether to create a task when there is no task," "when task creation may be skipped," or "whether sub-agents are required," edit these state blocks and the routing table above them.
## Local Modification Patterns
Common changes:
| Goal | Edit point |
| --- | --- |
| Add a phase | Update the Phase Index, phase body, routing, and state blocks. |
| Change task creation policy | Update the `no_task` state block and Phase 1 description. |
| Change the default implementation/check path | Update Phase 2 and skill routing. |
| Change the wrap-up flow | Update Phase 3 and `finish-work` related descriptions. Note the current split: Phase 3.4 = AI-driven code commits (batched, user-confirmed), Phase 3.5 = `/finish-work` (archive + record session). `/finish-work` refuses to run if the working tree is dirty. |
| Change platform differences | Update routing descriptions grouped by platform. |
After editing, make the AI reread `.trellis/workflow.md`; do not assume the flow from the old conversation is still valid.
## Relationship To Platform Files
`workflow.md` is the semantic center of the local workflow, but each platform can also have its own entry files:
- skills, such as `trellis-brainstorm` and `trellis-check`.
- commands/prompts/workflows, such as continue and finish-work.
- hooks, such as session-start or workflow-state injection.
If only `workflow.md` changes, platform entry files may still contain old language. When the user wants to change "what the AI actually does," also inspect the relevant platform directory.

View File

@@ -0,0 +1,71 @@
# Local Workspace Memory System
`.trellis/workspace/` stores cross-session memory. Its purpose is to let AI and humans understand what happened before across different windows and different days.
## Directory Structure
```text
.trellis/workspace/
├── index.md
└── <developer>/
├── index.md
├── journal-1.md
└── journal-2.md
```
| File | Purpose |
| --- | --- |
| `.trellis/.developer` | Current developer identity. |
| `.trellis/workspace/index.md` | Global workspace overview. |
| `.trellis/workspace/<developer>/index.md` | Session index for a developer. |
| `.trellis/workspace/<developer>/journal-N.md` | Session journal. |
## Developer Identity
Run this the first time:
```bash
python ./.trellis/scripts/init_developer.py <name>
```
This creates `.trellis/.developer` and the corresponding workspace directory. The AI should not change developer identity casually; if the identity is wrong, first confirm who is using the current project.
## Journal
`journal-N.md` records completed or partially completed work from each session. By default, each journal holds about 2000 lines; after that it rotates to the next file.
Common command for recording a session:
```bash
python ./.trellis/scripts/add_session.py \
--title "Session title" \
--summary "What changed" \
--commit "abc1234"
```
Planning or review work without a commit can also be recorded by using `--no-commit` or an empty commit value.
## Relationship Between Workspace Memory And Tasks
| System | What it stores |
| --- | --- |
| `.trellis/tasks/` | Requirements, design, research, and state for a specific task. |
| `.trellis/workspace/` | Work records across tasks and sessions. |
| `.trellis/spec/` | Engineering knowledge preserved as long-term conventions. |
If information is only useful for the current task, put it in the task directory.
If information describes what happened in the current session, put it in the workspace journal.
If information should be followed every time code is written in the future, put it in spec.
## Local Customization Points
| Need | Edit location |
| --- | --- |
| Change maximum journal lines | `max_journal_lines` in `.trellis/config.yaml`. |
| Change session auto-commit message | `session_commit_message` in `.trellis/config.yaml`. |
| Change session content format | `.trellis/scripts/add_session.py`. |
| Change how workspace is displayed in context | `.trellis/scripts/common/session_context.py`. |
## AI Usage Rules
The AI should not treat workspace as the only source of truth. When resuming a task, read the current task first, then use workspace for background. After a task is complete, record important process notes in workspace; if long-term rules emerged, update spec.

View File

@@ -0,0 +1,79 @@
# Agents
Trellis agent files define specialized roles. Common Trellis agents in a user project are:
- `trellis-research`
- `trellis-implement`
- `trellis-check`
File locations and formats differ by platform, but responsibility boundaries should stay consistent.
## Agent Responsibilities
| Agent | Responsibility |
| --- | --- |
| `trellis-research` | Investigate the question and write findings into the current task's `research/`. |
| `trellis-implement` | Implement against `prd.md`, `info.md`, `implement.jsonl`, and related spec/research. |
| `trellis-check` | Review changes, fix discovered issues, and run necessary checks. |
Agent files should not become generic chat prompts. They should define input sources, write boundaries, whether code may be changed, and how results are reported.
## Common Paths
| Platform | Agent path |
| --- | --- |
| Claude Code | `.claude/agents/trellis-*.md` |
| Cursor | `.cursor/agents/trellis-*.md` |
| OpenCode | `.opencode/agents/trellis-*.md` |
| Codex | `.codex/agents/trellis-*.toml` |
| Kiro | `.kiro/agents/trellis-*.json` |
| Gemini CLI | `.gemini/agents/trellis-*.md` |
| Qoder | `.qoder/agents/trellis-*.md` |
| CodeBuddy | `.codebuddy/agents/trellis-*.md` |
| Factory Droid | `.factory/droids/trellis-*.md` |
| Pi Agent | `.pi/agents/trellis-*.md` |
GitHub Copilot agent/prompt support is provided by a combination of directories such as `.github/agents/`, `.github/prompts/`, and `.github/skills/`; inspect the files actually generated in the user project.
Main-session workflow platforms such as Kilo, Antigravity, and Windsurf may not have Trellis sub-agent files. They usually rely on workflows/skills to guide the main session.
## Two Context Loading Modes
### hook push
The platform hook injects task context before the agent starts. The agent file itself can focus more on responsibilities and boundaries.
Common on platforms that support agent hooks.
### agent pull
The agent file instructs the agent to read after startup:
- `python ./.trellis/scripts/task.py current --source`
- current task `prd.md`
- `info.md`
- `implement.jsonl` or `check.jsonl`
- spec/research files referenced by JSONL
This mode fits platforms whose hooks cannot reliably rewrite sub-agent prompts.
## Local Change Scenarios
| User need | Edit location |
| --- | --- |
| Implement agent must follow extra restrictions | The platform's `trellis-implement` agent file. |
| Check agent must run project-specific commands | `trellis-check` agent file, and `.trellis/spec/` if needed. |
| Research agent must output a fixed format | `trellis-research` agent file. |
| Agent cannot read task context | Agent prelude or `inject-subagent-context` hook. |
| Add a project-specific agent | Platform agent directory + related workflow/command/skill entry point. |
## Modification Principles
1. **Keep responsibilities single-purpose**. Do not mix research, implement, and check responsibilities into one agent.
2. **Specify the read order**. Agents must know to start from the active task and then find the PRD and JSONL.
3. **Specify write boundaries**. Research usually only writes `research/`; implement can write code; check can fix issues.
4. **Keep semantics synchronized in multi-platform projects**. If the user configured Claude, Codex, and Cursor together, decide whether changes to one platform's agent also need to be applied to others.
## Do Not Default To Editing Upstream Templates
Local AI should default to modifying platform agent files inside the user project. Discuss upstream template source only when the user explicitly wants to contribute the change back to Trellis.

View File

@@ -0,0 +1,69 @@
# Hooks And Settings
Hooks/settings are the entry layer that connects a platform to Trellis. They decide which scripts, plugins, or extensions a platform runs for which events.
## Settings Responsibilities
settings/config files usually register:
- session-start hook: injects a Trellis overview when a new session starts or context resets.
- workflow-state hook: parses `[workflow-state:STATUS]` blocks from `.trellis/workflow.md` and emits the body matching the current task `status` on each user input. Parser-only; the script does not embed fallback content.
- sub-agent context hook: injects task context when implementation/check/research agents start.
- shell/session bridge: lets shell commands see the same Trellis session identity.
- platform plugin or extension entry points.
Common files:
| Platform | settings/config |
| --- | --- |
| Claude Code | `.claude/settings.json` |
| Cursor | `.cursor/hooks.json` |
| Codex | `.codex/hooks.json`, `.codex/config.toml` |
| OpenCode | `.opencode/package.json`, `.opencode/plugins/*` |
| Kiro | `.kiro/hooks/` + platform config |
| Gemini CLI | `.gemini/settings.json` |
| Qoder | `.qoder/settings.json` |
| CodeBuddy | `.codebuddy/settings.json` |
| GitHub Copilot | `.github/copilot/hooks.json` |
| Factory Droid | `.factory/settings.json` |
| Pi Agent | `.pi/settings.json`, `.pi/extensions/trellis/` |
Whether these files exist in a project depends on which `trellis init --<platform>` flags the user ran.
## Hook Script Types
| Script | Purpose |
| --- | --- |
| `session-start.py` | Generates session-start context. |
| `inject-workflow-state.py` | Parses `[workflow-state:STATUS]` blocks in `.trellis/workflow.md` and emits the body matching the current task status. Falls back to `Refer to workflow.md for current step.` when no matching block exists. |
| `inject-subagent-context.py` | Injects PRD, JSONL context, and related spec/research into sub-agents. |
| `inject-shell-session-context.py` | Lets shell commands inherit Trellis session identity. |
Not every platform has every hook. Do not copy files from another platform just because a platform lacks a hook; first confirm whether that platform supports the corresponding event.
## Local Change Scenarios
| User need | Edit location |
| --- | --- |
| AI should see more/less context in a new session | Platform `session-start` hook. |
| Per-turn hint policy should change | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook parses workflow.md verbatim — no script edit required. |
| Sub-agent cannot read PRD/spec | `inject-subagent-context` hook or agent prelude. |
| `task.py current` in shell has no active task | Shell/session bridge hook or platform environment variable configuration. |
| Disable an automatic injection | The corresponding hook registration in settings/config. |
## Modification Principles
1. **Settings wire things up; hooks define behavior**. If only the hook changes, the platform may never call it. If only settings change, behavior may not change.
2. **Confirm platform event names first**. Different platforms use different names for SessionStart, UserPromptSubmit, AgentSpawn, shell execution, and similar events.
3. **Hooks read local `.trellis/`, not upstream source**. `.trellis/scripts/` and `.trellis/workflow.md` in the user project are the default targets.
4. **Errors must be visible**. Hook failures should tell the user what was not injected instead of silently leaving the AI without context.
## Troubleshooting Path
If the user says "AI did not read Trellis state":
1. Check whether the platform settings register the hook.
2. Check whether the hook file exists.
3. Manually run the `.trellis/scripts/get_context.py` or `task.py current --source` command that the hook depends on.
4. Check whether active task state exists in `.trellis/.runtime/sessions/`.
5. Check whether the platform shell passes session identity.

View File

@@ -0,0 +1,59 @@
# Platform Files Overview
Trellis connects the same local architecture to different AI tools. `.trellis/` stores the shared runtime; platform directories store adapter files that define how each AI tool enters Trellis.
When a local AI modifies Trellis, it should distinguish two file categories first:
- **Shared files**: `.trellis/workflow.md`, `.trellis/tasks/`, `.trellis/spec/`, `.trellis/scripts/`.
- **Platform files**: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories.
Platform files do not store business state. They let the corresponding AI tool read Trellis state, call Trellis scripts, and load Trellis skills/agents/hooks.
## Platform File Categories
| Category | Common paths | Purpose |
| --- | --- | --- |
| settings/config | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json` | Register hooks, plugins, extensions, or platform behavior. |
| hooks/plugins/extensions | `.claude/hooks/`, `.opencode/plugins/`, `.pi/extensions/` | Inject context at session start, user input, agent startup, shell execution, and similar events. |
| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/` | Define `trellis-research`, `trellis-implement`, and `trellis-check`. |
| skills | `.claude/skills/`, `.agents/skills/`, `.qoder/skills/` | Capability descriptions that auto-trigger or can be read on demand. |
| commands/prompts/workflows | `.cursor/commands/`, `.github/prompts/`, `.windsurf/workflows/` | Entry points explicitly invoked by the user. |
## Three Platform Integration Modes
### 1. Hook / Extension Driven
These platforms can trigger scripts or plugins on specific events and actively inject Trellis context into AI.
Common capabilities:
- session-start injection of a `.trellis/` overview.
- workflow-state hints for each user turn.
- PRD/spec/research injection when sub-agents start.
- Shell commands inheriting session identity.
To change "when the AI knows what," inspect hooks/plugins/extensions and settings first.
### 2. Agent Prelude / Pull-Based
Some platforms cannot reliably let hooks rewrite sub-agent prompts, so the agent file itself instructs the agent to read the active task, PRD, and JSONL context after startup.
To change how sub-agents load context, inspect the agent files themselves.
### 3. Main-Session Workflow
Some platforms do not have Trellis sub-agent or hook capabilities. They rely on workflows/skills/commands to guide the main-session AI to read files, run scripts, and move tasks forward.
To change behavior, inspect platform workflows/skills/commands and `.trellis/workflow.md`.
## Local Modification Order
When the user asks to customize behavior for a platform, the AI should inspect files in this order:
1. Read `.trellis/workflow.md` to confirm the shared flow.
2. Read the target platform's settings/config to see which hooks/agents/skills/commands are registered.
3. Read the target platform's agents/skills/commands/hooks.
4. Modify the local file closest to the user's need.
5. If the change affects the shared flow, synchronize `.trellis/workflow.md` or `.trellis/spec/`.
Do not modify only platform files and forget the shared workflow. Do not modify only `.trellis/workflow.md` and forget that platform entry points may still contain old descriptions.

View File

@@ -0,0 +1,74 @@
# Platform File Map
This page lists common Trellis file locations in a user project by platform. Whether a platform directory exists in an actual project depends on which `trellis init --<platform>` commands the user ran.
## Matrix
| Platform | CLI flag | Main directory | Skill directory | Agent directory | Hooks/extensions |
| --- | --- | --- | --- | --- | --- |
| Claude Code | `--claude` | `.claude/` | `.claude/skills/` | `.claude/agents/` | `.claude/hooks/` + `.claude/settings.json` |
| Cursor | `--cursor` | `.cursor/` | `.cursor/skills/` | `.cursor/agents/` | `.cursor/hooks.json` + `.cursor/hooks/` |
| OpenCode | `--opencode` | `.opencode/` | `.opencode/skills/` | `.opencode/agents/` | `.opencode/plugins/` |
| Codex | `--codex` | `.codex/` | `.agents/skills/` | `.codex/agents/` | `.codex/hooks/` + `.codex/hooks.json` |
| Kilo | `--kilo` | `.kilocode/` | `.kilocode/skills/` | Usually none | `.kilocode/workflows/` |
| Kiro | `--kiro` | `.kiro/` | `.kiro/skills/` | `.kiro/agents/` | `.kiro/hooks/` |
| Gemini CLI | `--gemini` | `.gemini/` | `.agents/skills/` | `.gemini/agents/` | `.gemini/settings.json` + `.gemini/hooks/` |
| Antigravity | `--antigravity` | `.agent/` | `.agent/skills/` | Usually none | `.agent/workflows/` |
| Windsurf | `--windsurf` | `.windsurf/` | `.windsurf/skills/` | Usually none | `.windsurf/workflows/` |
| Qoder | `--qoder` | `.qoder/` | `.qoder/skills/` | `.qoder/agents/` | `.qoder/hooks/` + `.qoder/settings.json` |
| CodeBuddy | `--codebuddy` | `.codebuddy/` | `.codebuddy/skills/` | `.codebuddy/agents/` | `.codebuddy/hooks/` + `.codebuddy/settings.json` |
| GitHub Copilot | `--copilot` | `.github/` | `.github/skills/` | `.github/agents/` | `.github/copilot/hooks/` + prompts |
| Factory Droid | `--droid` | `.factory/` | `.factory/skills/` | `.factory/droids/` | `.factory/hooks/` + settings |
| Pi Agent | `--pi` | `.pi/` | `.pi/skills/` | `.pi/agents/` | `.pi/extensions/trellis/` + `.pi/settings.json` |
## Capability Groups
### Trellis Sub-Agent Support
These platforms usually have `trellis-research`, `trellis-implement`, and `trellis-check` files:
- Claude Code
- Cursor
- OpenCode
- Codex
- Kiro
- Gemini CLI
- Qoder
- CodeBuddy
- GitHub Copilot
- Factory Droid
- Pi Agent
When changing implementation/check/research behavior, look for the corresponding platform agent files first.
### Main-Session Workflow Platforms
These platforms rely more on workflows/skills to guide the main session:
- Kilo
- Antigravity
- Windsurf
When changing behavior, inspect workflows and skills first. Do not assume Trellis sub-agents exist.
### Shared `.agents/skills/`
Codex writes the shared `.agents/skills/` layer. Some tools that support agentskills.io can also read this directory. If the user wants multiple compatible tools to share one skill, consider `.agents/skills/` first, but do not assume every platform reads it.
## Decision Rules When Modifying Platform Files
1. User specified a platform: modify only that platform directory unless shared workflow/spec files must also change.
2. User says "all platforms should do this": synchronize equivalent entry points platform by platform; do not modify only one directory.
3. User only says "my AI": inspect the configuration directories that actually exist in the project and infer the current AI platform.
4. User wants project rules: prefer `.trellis/spec/` or a project-local skill.
5. User wants Trellis behavior: edit `.trellis/workflow.md` plus platform hooks/agents/skills/commands.
## When Paths Differ
Platform ecosystems change, and user projects may already be customized. If this table disagrees with local files, use the actual settings/config in the user project as authoritative:
- Check the hook that settings registers.
- Check the script that a command/prompt/workflow points to.
- Judge behavior by the read rules currently written in the agent file.
Do not delete a custom file just because it is not listed in this path table.

View File

@@ -0,0 +1,83 @@
# Skills, Commands, Prompts, And Workflows
Skills and commands are textual entry points for user interaction with Trellis. Different platforms use different names, but their core purpose is the same: tell the AI how to enter the Trellis flow when the user expresses a certain intent.
## Conceptual Differences
| Type | Trigger mode | Best for |
| --- | --- | --- |
| skill | AI auto-match or explicit user mention | Long-term capabilities, workflow rules, modification guides. |
| command | Explicit user invocation | Clear operation entry points such as continue and finish-work. |
| prompt | Explicit user invocation or platform selection | Similar to command, but in a platform prompt format. |
| workflow | Explicit user selection or platform auto-match | Guides the main session when no sub-agent/hook exists. |
Trellis workflow skills usually share one semantic set: brainstorm, before-dev, check, update-spec, break-loop. Multi-file built-in skills such as `trellis-meta` use layered references.
## Common Paths
| Platform | Common entries |
| --- | --- |
| Claude Code | `.claude/skills/`, `.claude/commands/` |
| Cursor | `.cursor/skills/`, `.cursor/commands/` |
| OpenCode | `.opencode/skills/`, `.opencode/commands/` |
| Codex | `.agents/skills/`, `.codex/skills/` |
| Kilo | `.kilocode/skills/`, `.kilocode/workflows/` |
| Kiro | `.kiro/skills/` |
| Gemini CLI | `.agents/skills/`, `.gemini/commands/` |
| Antigravity | `.agent/skills/`, `.agent/workflows/` |
| Windsurf | `.windsurf/skills/`, `.windsurf/workflows/` |
| Qoder | `.qoder/skills/`, `.qoder/commands/` |
| CodeBuddy | `.codebuddy/skills/`, `.codebuddy/commands/` |
| GitHub Copilot | `.github/skills/`, `.github/prompts/` |
| Factory Droid | `.factory/skills/`, `.factory/commands/` |
| Pi Agent | `.pi/skills/` |
In a user project, use the files actually generated by init as authoritative.
## Skill Structure
A common skill is a directory:
```text
trellis-meta/
├── SKILL.md
└── references/
```
`SKILL.md` should tell the AI:
- When to use this skill.
- Which reference to read first for the current task.
- What not to do.
References hold longer explanations so the entry file does not contain everything.
## Command/Prompt/Workflow Structure
Commands, prompts, and workflows are usually single files. Their content should include:
- When to use it.
- Which `.trellis/` files to read.
- Which scripts to run.
- How to report after completion.
They should not store task state; task state belongs in `.trellis/tasks/` and `.trellis/.runtime/`.
## Local Change Scenarios
| User need | Edit location |
| --- | --- |
| Change AI auto-trigger rules | The corresponding skill's frontmatter description. |
| Change user command behavior | The corresponding command/prompt/workflow file. |
| Add a project-local skill | Platform skill directory, or shared `.agents/skills/`. |
| Let multiple platforms share one capability | Write equivalent skills in each platform skill directory, or use the `.agents/skills/` shared layer on platforms that support it. |
| Change finish/continue entry points | Platform commands/prompts/workflows. |
## Modification Principles
1. **Keep entry files short; references carry long content**. This matters especially for multi-file skills like `trellis-meta`.
2. **Make trigger descriptions specific**. A description that is too broad can mis-trigger; one that is too narrow may not trigger.
3. **Keep the same semantics consistent across platforms**. File formats can differ, but behavior descriptions should match.
4. **Put project-specific capabilities in local skills**. Do not put team-private flows into public `trellis-meta`.
If the user only wants local AI to know one more project rule, usually create a project-local skill or update `.trellis/spec/` instead of changing a Trellis built-in workflow skill.

View File

@@ -0,0 +1,356 @@
---
name: trellis-update-spec
description: "Captures executable contracts and coding conventions into .trellis/spec/ documents. Use when learning something valuable from debugging, implementing, or discussion that should be preserved for future sessions."
---
# Update Code-Spec - Capture Executable Contracts
When you learn something valuable (from debugging, implementing, or discussion), use this to update the relevant code-spec documents.
**Timing**: After completing a task, fixing a bug, or discovering a new pattern
---
## Code-Spec First Rule (CRITICAL)
In this project, "spec" for implementation work means **code-spec**:
- Executable contracts (not principle-only text)
- Concrete signatures, payload fields, env keys, and boundary behavior
- Testable validation/error behavior
If the change touches infra or cross-layer contracts, code-spec depth is mandatory.
### Mandatory Triggers
Apply code-spec depth when the change includes any of:
- New/changed command or API signature
- Cross-layer request/response contract change
- Database schema/migration change
- Infra integration (storage, queue, cache, secrets, env wiring)
### Mandatory Output (7 Sections)
For triggered tasks, include all sections below:
1. Scope / Trigger
2. Signatures (command/API/DB)
3. Contracts (request/response/env)
4. Validation & Error Matrix
5. Good/Base/Bad Cases
6. Tests Required (with assertion points)
7. Wrong vs Correct (at least one pair)
---
## When to Update Code-Specs
| Trigger | Example | Target Spec |
|---------|---------|-------------|
| **Implemented a feature** | Added a new integration or module | Relevant spec file |
| **Made a design decision** | Chose extensibility pattern over simplicity | Relevant spec + "Design Decisions" section |
| **Fixed a bug** | Found a subtle issue with error handling | Relevant spec (e.g., error-handling docs) |
| **Discovered a pattern** | Found a better way to structure code | Relevant spec file |
| **Hit a gotcha** | Learned that X must be done before Y | Relevant spec + "Common Mistakes" section |
| **Established a convention** | Team agreed on naming pattern | Quality guidelines |
| **New thinking trigger** | "Don't forget to check X before doing Y" | `guides/*.md` (as a checklist item) |
**Key Insight**: Code-spec updates are NOT just for problems. Every feature implementation contains design decisions and contracts that future AI/developers need to execute safely.
---
## Spec Structure Overview
```
.trellis/spec/
├── <layer>/ # Per-layer coding standards (e.g., backend/, frontend/, api/)
│ ├── index.md # Overview and links
│ └── *.md # Topic-specific guidelines
└── guides/ # Thinking checklists (NOT coding specs!)
├── index.md # Guide index
└── *.md # Topic-specific guides
```
### CRITICAL: Code-Spec vs Guide - Know the Difference
| Type | Location | Purpose | Content Style |
|------|----------|---------|---------------|
| **Code-Spec** | `<layer>/*.md` | Tell AI "how to implement safely" | Signatures, contracts, matrices, cases, test points |
| **Guide** | `guides/*.md` | Help AI "what to think about" | Checklists, questions, pointers to specs |
**Decision Rule**: Ask yourself:
- "This is **how to write** the code" → Put in a spec layer directory
- "This is **what to consider** before writing" → Put in `guides/`
**Example**:
| Learning | Wrong Location | Correct Location |
|----------|----------------|------------------|
| "Use API X not API Y for this task" | ❌ `guides/` (too specific for a thinking guide) | ✅ Relevant spec file (concrete convention) |
| "Remember to check X when doing Y" | ❌ Spec file (too abstract for a spec) | ✅ `guides/` (thinking checklist) |
**Guides should be short checklists that point to specs**, not duplicate the detailed rules.
---
## Update Process
### Step 1: Identify What You Learned
Answer these questions:
1. **What did you learn?** (Be specific)
2. **Why is it important?** (What problem does it prevent?)
3. **Where does it belong?** (Which spec file?)
### Step 2: Classify the Update Type
| Type | Description | Action |
|------|-------------|--------|
| **Design Decision** | Why we chose approach X over Y | Add to "Design Decisions" section |
| **Project Convention** | How we do X in this project | Add to relevant section with examples |
| **New Pattern** | A reusable approach discovered | Add to "Patterns" section |
| **Forbidden Pattern** | Something that causes problems | Add to "Anti-patterns" or "Don't" section |
| **Common Mistake** | Easy-to-make error | Add to "Common Mistakes" section |
| **Convention** | Agreed-upon standard | Add to relevant section |
| **Gotcha** | Non-obvious behavior | Add warning callout |
### Step 3: Read the Target Code-Spec
Before editing, read the current code-spec to:
- Understand existing structure
- Avoid duplicating content
- Find the right section for your update
```bash
cat .trellis/spec/<category>/<file>.md
```
### Step 4: Make the Update
Follow these principles:
1. **Be Specific**: Include concrete examples, not just abstract rules
2. **Explain Why**: State the problem this prevents
3. **Show Contracts**: Add signatures, payload fields, and error behavior
4. **Show Code**: Add code snippets for key patterns
5. **Keep it Short**: One concept per section
### Step 5: Update the Index (if needed)
If you added a new section or the code-spec status changed, update the category's `index.md`.
---
## Update Templates
### Mandatory Template for Infra/Cross-Layer Work
```markdown
## Scenario: <name>
### 1. Scope / Trigger
- Trigger: <why this requires code-spec depth>
### 2. Signatures
- Backend command/API/DB signature(s)
### 3. Contracts
- Request fields (name, type, constraints)
- Response fields (name, type, constraints)
- Environment keys (required/optional)
### 4. Validation & Error Matrix
- <condition> -> <error>
### 5. Good/Base/Bad Cases
- Good: ...
- Base: ...
- Bad: ...
### 6. Tests Required
- Unit/Integration/E2E with assertion points
### 7. Wrong vs Correct
#### Wrong
...
#### Correct
...
```
### Adding a Design Decision
```markdown
### Design Decision: [Decision Name]
**Context**: What problem were we solving?
**Options Considered**:
1. Option A - brief description
2. Option B - brief description
**Decision**: We chose Option X because...
**Example**:
\`\`\`typescript
// How it's implemented
code example
\`\`\`
**Extensibility**: How to extend this in the future...
```
### Adding a Project Convention
```markdown
### Convention: [Convention Name]
**What**: Brief description of the convention.
**Why**: Why we do it this way in this project.
**Example**:
\`\`\`typescript
// How to follow this convention
code example
\`\`\`
**Related**: Links to related conventions or specs.
```
### Adding a New Pattern
```markdown
### Pattern Name
**Problem**: What problem does this solve?
**Solution**: Brief description of the approach.
**Example**:
\`\`\`
// Good
code example
// Bad
code example
\`\`\`
**Why**: Explanation of why this works better.
```
### Adding a Forbidden Pattern
```markdown
### Don't: Pattern Name
**Problem**:
\`\`\`
// Don't do this
bad code example
\`\`\`
**Why it's bad**: Explanation of the issue.
**Instead**:
\`\`\`
// Do this instead
good code example
\`\`\`
```
### Adding a Common Mistake
```markdown
### Common Mistake: Description
**Symptom**: What goes wrong
**Cause**: Why this happens
**Fix**: How to correct it
**Prevention**: How to avoid it in the future
```
### Adding a Gotcha
```markdown
> **Warning**: Brief description of the non-obvious behavior.
>
> Details about when this happens and how to handle it.
```
---
## Interactive Mode
If you're unsure what to update, answer these prompts:
1. **What did you just finish?**
- [ ] Fixed a bug
- [ ] Implemented a feature
- [ ] Refactored code
- [ ] Had a discussion about approach
2. **What did you learn or decide?**
- Design decision (why X over Y)
- Project convention (how we do X)
- Non-obvious behavior (gotcha)
- Better approach (pattern)
3. **Would future AI/developers need to know this?**
- To understand how the code works → Yes, update spec
- To maintain or extend the feature → Yes, update spec
- To avoid repeating mistakes → Yes, update spec
- Purely one-off implementation detail → Maybe skip
4. **Which area does it relate to?**
- [ ] Backend code
- [ ] Frontend code
- [ ] Cross-layer data flow
- [ ] Code organization/reuse
- [ ] Quality/testing
---
## Quality Checklist
Before finishing your code-spec update:
- [ ] Is the content specific and actionable?
- [ ] Did you include a code example?
- [ ] Did you explain WHY, not just WHAT?
- [ ] Did you include executable signatures/contracts?
- [ ] Did you include validation and error matrix?
- [ ] Did you include Good/Base/Bad cases?
- [ ] Did you include required tests with assertion points?
- [ ] Is it in the right code-spec file?
- [ ] Does it duplicate existing content?
- [ ] Would a new team member understand it?
---
## Relationship to Other Commands
```
Development Flow:
Learn something → /trellis-update-spec → Knowledge captured
↑ ↓
/trellis-break-loop ←──────────────────── Future sessions benefit
(deep bug analysis)
```
- `/trellis-break-loop` - Analyzes bugs deeply, often reveals spec updates needed
- `/trellis-update-spec` - Actually makes the updates
- `/trellis-finish-work` - Reminds you to check if specs need updates
---
## Core Philosophy
> **Code-specs are living documents. Every debugging session, every "aha moment" is an opportunity to make the implementation contract clearer.**
The goal is **institutional memory**:
- What one person learns, everyone benefits from
- What AI learns in one session, persists to future sessions
- Mistakes become documented guardrails

2
.gitignore vendored
View File

@@ -16,6 +16,8 @@
# next.js # next.js
/.next/ /.next/
/out/ /out/
/frontend
/.idea
# production # production
/build /build

32
.trellis/.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Developer identity (local only)
.developer
# Current task pointer (each dev works on different task)
.current-task
# Session/window scoped runtime state
.runtime/
# Ralph Loop state file
.ralph-state.json
# Agent runtime files
.agents/
.agent-log
.session-id
# Task directory runtime files
.plan-log
# Atomic update temp files
*.tmp
# Update backup directories
.backup-*
# Conflict resolution temp files
*.new
# Python cache
**/__pycache__/
**/*.pyc

View File

@@ -0,0 +1,107 @@
{
"__version": 2,
"hashes": {
"AGENTS.md": "e00bbed329e0fc9a4931d3fb8a3c7b47c7311a7b5a39f38dcfb44f2c63fbd390",
".trellis/config.yaml": "c3c4af7d82c09a1638f63c1f560119507735b060a4780ef7e6d0cdef447c215d",
".trellis/scripts/add_session.py": "cb5fadbca9e4af389e44e3d536797a8a01a16dac325742582a91fdaa19ebd3cb",
".trellis/scripts/common/active_task.py": "6c88ed40ef7289bca0f6d2ecba0f8b8aef46cd58788080fbeeea88de138a431f",
".trellis/scripts/common/cli_adapter.py": "cd844d1e84b1a09b373b3a7609e4d5606ee9d4825154c002cc9bb3f54c8e2fb9",
".trellis/scripts/common/config.py": "671a3591f97b75ec19f25814d2ee3f7e9b38e048f6f67442519fe0715c454eeb",
".trellis/scripts/common/developer.py": "b2141b0145a41f8cedb4f9a24c925796edb2f0f6fde7c86b559513ec30499368",
".trellis/scripts/common/git.py": "e14817be7de122d3a106f509c2825aeb9669d962ba73ba241642d2931cfdf1d6",
".trellis/scripts/common/git_context.py": "7533c08335791e50c3a6f9d551d5b1af0bdaa2a0a746721cb3e1a2140f4d9683",
".trellis/scripts/common/io.py": "6480b181f2bc505323b28ed7a66963d7b7edc96251e83b4c8e7a45907cc721c8",
".trellis/scripts/common/log.py": "471df6895cfac80f995edebbf9974f6b7440634b7a688f28b8331c868bc0f3cf",
".trellis/scripts/common/packages_context.py": "efe158d7c99c2268851d0216fbb08de22836e418a8dbeb73575b8cc249eed7b7",
".trellis/scripts/common/paths.py": "05898ef136cc7c4d861b05fbf2b16d53ddd3e6f311a231d4fcfcb81bde7c45ee",
".trellis/scripts/common/session_context.py": "04537f395ec2b2f75bcfef8f73ba75e5a215d99781927b72bc678f5bf349122b",
".trellis/scripts/common/tasks.py": "4436a8b0b53c270a35989e26d9dbd92669408c6562d88c02083a404562da85fe",
".trellis/scripts/common/task_context.py": "1c16a7fa82d363010d0d0ebdc038296ae1552bf6e90214787d707f49567bc159",
".trellis/scripts/common/task_queue.py": "0be61f713462b1fe4574927c82fc4704e678afe72dcb9813543aedf2f9e9e0c5",
".trellis/scripts/common/task_store.py": "9e6bd54669e11de99ca1174ec3fc620feb9697c454672db440ff4cabd1fac7fb",
".trellis/scripts/common/task_utils.py": "f5ef4af87ba3e11d8b19630c0c96d009de1811fc9be56c2027a9c96e21ed103e",
".trellis/scripts/common/types.py": "9962081cc2608fb9d1deb32c6880e336f62cdca6b338e7ae813304701e155ee9",
".trellis/scripts/common/workflow_phase.py": "3ca97e634b53a428206b04f87eba1700d4b2063cf367ee276ab0b1849994b81d",
".trellis/scripts/common/__init__.py": "3d5e9347141f0296319a5beb29d69ae714c5a474b9078caeb3edd7c5f6562e22",
".trellis/scripts/get_context.py": "af3ea7cd563a453227cf2cb4ab04d667390046b7febfac2217348d0892781f4b",
".trellis/scripts/get_developer.py": "84c27076323c3e0f2c9c8ed16e8aa865e225d902a187c37e20ee1a46e7142d8f",
".trellis/scripts/hooks/linear_sync.py": "cfc270b7ff775caa5b2434823c45414a3b37f9ba2aa1e293a26daef9fd2e577a",
".trellis/scripts/init_developer.py": "0943f1c240993649ab89b91a2c5b379e84daa8c53b35f0490774bff05a552873",
".trellis/scripts/task.py": "e2614fbfc1308c90c0708a11475ca6684ea0a1e2a845140300192229589a2f1f",
".trellis/scripts/__init__.py": "1242be5b972094c2e141aecbe81a4efd478f6534e3d5e28306374e6a18fcf46c",
".trellis/workflow.md": "07256ef6e2fd6f2db3cba046514f9e00c30a388b0976094581b47bf1543e383c",
".claude/agents/trellis-check.md": "d1359521f7f3e9bbbf10e856a3e0912c423581a88ac188b1f0523d6357962909",
".claude/agents/trellis-implement.md": "61155f06ccdd26e5aeb8171face2a029a8fb77a3d1a2b277442ded186853446c",
".claude/agents/trellis-research.md": "f95e69d638266056713e79c884ead1e99d376d70284f66255b6dd139a3e712be",
".claude/commands/trellis/continue.md": "e609e940236f33a9b05a15173606cc3f72285994904d6723382c490d94994aba",
".claude/commands/trellis/finish-work.md": "f11f661cff6d5d26dccb5e9574c3d2c7873a9dfaed7962d471ff5ea2fd48d691",
".claude/hooks/inject-subagent-context.py": "d8d69631b43ef469030ff78410c2e082f721f79b7d6eaa754b7a1a1c05810242",
".claude/hooks/inject-workflow-state.py": "0684fb17d0d42b36d1549e9bc0a905d4c06f714e2b2008d74d9ab0d2c1c2b626",
".claude/hooks/session-start.py": "c43314d810937c4fafb65af7128fd01f01c367c4378aef6756a5237bed78d585",
".claude/settings.json": "27ad116000465c63b2c572e05bbdcf3a3c340614cc9cb62fb5c112b29f0f96bc",
".claude/skills/trellis-before-dev/SKILL.md": "310e0121d5915a8aa46596fc172b53a7bdbaae4fd11699500e3166783a15a180",
".claude/skills/trellis-brainstorm/SKILL.md": "942c54edcd2e499fa031a7ab017a78a36e0c6ddb3138a3b1256eee95ec5227d5",
".claude/skills/trellis-break-loop/SKILL.md": "35afb53fef42cd494e566f1ef170dbf442ec2be7e19931f28a14079b4dda753f",
".claude/skills/trellis-check/SKILL.md": "8ce33f85051a339e77722bab214562ba7aa041629e285381434bf51c7c710205",
".claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md": "ef3380e71aa9f5103d37b467b1f725a8033ac516e4de31e4d790be02ec2c39e8",
".claude/skills/trellis-meta/references/customize-local/change-agents.md": "7f2982162463f107f8b1a4fa1a41fee2bc7dbd0cc8e90c48559aba30c3ea403c",
".claude/skills/trellis-meta/references/customize-local/change-context-loading.md": "aacdaaca13a4420b9fddf0023d90d3bf06d4aa96ae51c44a201f81b3f3723088",
".claude/skills/trellis-meta/references/customize-local/change-hooks.md": "c8b35dda1530de521cf6bb043188f0cbbea0c9180b1aa44e64e31e20433ef4ca",
".claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "b3009ef20a4f24e5d8b196109dc9bab6bd30fc030dbc4fb796afdd2ca912e1ea",
".claude/skills/trellis-meta/references/customize-local/change-spec-structure.md": "b6facc3976df445ff478ca06459b87b67b7c494b98ccfc53a55bdb78a079babf",
".claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md": "148b7442ef8106de907afd06f9d1ca96f7ec074caedced3dd4175b3a26698ca2",
".claude/skills/trellis-meta/references/customize-local/change-workflow.md": "6f1707a2cc032c50e41e5624cef46071dd53dc9810bc6b3cae66d86508dea1cb",
".claude/skills/trellis-meta/references/customize-local/overview.md": "465db9cecf085b37f7aed2fc5240c92c638e937f7960ca35b0f05a780dd4fdc9",
".claude/skills/trellis-meta/references/local-architecture/context-injection.md": "31286b9c05e600db7d179100eca533f9b8a4aab3a9c255cb69e8dccacb4e8375",
".claude/skills/trellis-meta/references/local-architecture/generated-files.md": "4356517517cef0ba7f3ba01965a4ba8953505702e4085f0797d3e36817c9669f",
".claude/skills/trellis-meta/references/local-architecture/overview.md": "45ffd4ee95020f58201adc885f3dfc89b26483c2b350d96ca7f2f57f94d5ff5f",
".claude/skills/trellis-meta/references/local-architecture/spec-system.md": "dd53adaf18374c8ce598092a24847c43a4e661b2708c379615e86defd21f107d",
".claude/skills/trellis-meta/references/local-architecture/task-system.md": "c80af5ae864b86c33eac4442d1244451f6cbcf5f87effccd17cd1856aa00315e",
".claude/skills/trellis-meta/references/local-architecture/workflow.md": "cfcdc6e4468a5d9c816e929fcca01640cd41cfdaaa4824118b40a8e460c927b6",
".claude/skills/trellis-meta/references/local-architecture/workspace-memory.md": "79786a1ca2980b1785a36aba8142f9d879459c47dc000c999f638e5c864d04d3",
".claude/skills/trellis-meta/references/platform-files/agents.md": "700e1b7ba89b304f0ee7d26528d897f0c66e382801913e20b59323651f5ca675",
".claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "6e2d6d88719c2779fe34004f63d36cff203d8f64e7fb620f7cb1cde15c37c462",
".claude/skills/trellis-meta/references/platform-files/overview.md": "6479cd2393166b4b369b511c44b78cbc64975c8b1df96ee1d4d1bd06b75cd48d",
".claude/skills/trellis-meta/references/platform-files/platform-map.md": "ded6751c06f31d0a701d33c9dd69c482a583539ad3ed464aaad9e705f793b212",
".claude/skills/trellis-meta/references/platform-files/skills-and-commands.md": "85435eb8bb6921283575bca51268fc534c22fd3ca33782e841ee5c76140ae48f",
".claude/skills/trellis-meta/SKILL.md": "942e898a6fd769a93a3ca6f43f9fe0412d0adae011654fd384e9cacbd2af4f34",
".claude/skills/trellis-update-spec/SKILL.md": "d975db7af166578488958751ae2c56edb827a68bddb569aa27acc3453f64e610",
".cursor/agents/trellis-check.md": "dfb3e3af324f21d9c8af377a2f25cdf5cd37ae062c0433205d3a68cb5b45ed1a",
".cursor/agents/trellis-implement.md": "2b52d7c4a0a67be4dd0c85c89159a917293014cafeecc2f3a9549b1ac31ceee3",
".cursor/agents/trellis-research.md": "c4192b6207d2797744bf0a3faffce381fe609366b0aa5c654d2058eb59823a4e",
".cursor/commands/trellis-continue.md": "fe7a89c1e4f62634650781476e58988a3fc3b82bcce34d8f862374f74685d5a1",
".cursor/commands/trellis-finish-work.md": "16006238f2b8410a87fd8b857b4ed3f9f529b28cd85fe97cfeabe79f95406e41",
".cursor/hooks/inject-shell-session-context.py": "28502dd7cb657fed92005c2e1c60334ce216545c40d4791b9433cbf779f83968",
".cursor/hooks/inject-subagent-context.py": "d8d69631b43ef469030ff78410c2e082f721f79b7d6eaa754b7a1a1c05810242",
".cursor/hooks/inject-workflow-state.py": "0684fb17d0d42b36d1549e9bc0a905d4c06f714e2b2008d74d9ab0d2c1c2b626",
".cursor/hooks/session-start.py": "c43314d810937c4fafb65af7128fd01f01c367c4378aef6756a5237bed78d585",
".cursor/hooks.json": "920c7b5caa5c8108239790c827526deba6e7c96e187bfc089cc66e5b1436809a",
".cursor/skills/trellis-before-dev/SKILL.md": "310e0121d5915a8aa46596fc172b53a7bdbaae4fd11699500e3166783a15a180",
".cursor/skills/trellis-brainstorm/SKILL.md": "a07c8ee9a873a07535921e6675b32487c9e5984ecc70cfe979ceb4754f2300b0",
".cursor/skills/trellis-break-loop/SKILL.md": "35afb53fef42cd494e566f1ef170dbf442ec2be7e19931f28a14079b4dda753f",
".cursor/skills/trellis-check/SKILL.md": "8ce33f85051a339e77722bab214562ba7aa041629e285381434bf51c7c710205",
".cursor/skills/trellis-meta/references/customize-local/add-project-local-conventions.md": "ef3380e71aa9f5103d37b467b1f725a8033ac516e4de31e4d790be02ec2c39e8",
".cursor/skills/trellis-meta/references/customize-local/change-agents.md": "7f2982162463f107f8b1a4fa1a41fee2bc7dbd0cc8e90c48559aba30c3ea403c",
".cursor/skills/trellis-meta/references/customize-local/change-context-loading.md": "aacdaaca13a4420b9fddf0023d90d3bf06d4aa96ae51c44a201f81b3f3723088",
".cursor/skills/trellis-meta/references/customize-local/change-hooks.md": "c8b35dda1530de521cf6bb043188f0cbbea0c9180b1aa44e64e31e20433ef4ca",
".cursor/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "b3009ef20a4f24e5d8b196109dc9bab6bd30fc030dbc4fb796afdd2ca912e1ea",
".cursor/skills/trellis-meta/references/customize-local/change-spec-structure.md": "b6facc3976df445ff478ca06459b87b67b7c494b98ccfc53a55bdb78a079babf",
".cursor/skills/trellis-meta/references/customize-local/change-task-lifecycle.md": "148b7442ef8106de907afd06f9d1ca96f7ec074caedced3dd4175b3a26698ca2",
".cursor/skills/trellis-meta/references/customize-local/change-workflow.md": "6f1707a2cc032c50e41e5624cef46071dd53dc9810bc6b3cae66d86508dea1cb",
".cursor/skills/trellis-meta/references/customize-local/overview.md": "465db9cecf085b37f7aed2fc5240c92c638e937f7960ca35b0f05a780dd4fdc9",
".cursor/skills/trellis-meta/references/local-architecture/context-injection.md": "31286b9c05e600db7d179100eca533f9b8a4aab3a9c255cb69e8dccacb4e8375",
".cursor/skills/trellis-meta/references/local-architecture/generated-files.md": "4356517517cef0ba7f3ba01965a4ba8953505702e4085f0797d3e36817c9669f",
".cursor/skills/trellis-meta/references/local-architecture/overview.md": "45ffd4ee95020f58201adc885f3dfc89b26483c2b350d96ca7f2f57f94d5ff5f",
".cursor/skills/trellis-meta/references/local-architecture/spec-system.md": "dd53adaf18374c8ce598092a24847c43a4e661b2708c379615e86defd21f107d",
".cursor/skills/trellis-meta/references/local-architecture/task-system.md": "c80af5ae864b86c33eac4442d1244451f6cbcf5f87effccd17cd1856aa00315e",
".cursor/skills/trellis-meta/references/local-architecture/workflow.md": "cfcdc6e4468a5d9c816e929fcca01640cd41cfdaaa4824118b40a8e460c927b6",
".cursor/skills/trellis-meta/references/local-architecture/workspace-memory.md": "79786a1ca2980b1785a36aba8142f9d879459c47dc000c999f638e5c864d04d3",
".cursor/skills/trellis-meta/references/platform-files/agents.md": "700e1b7ba89b304f0ee7d26528d897f0c66e382801913e20b59323651f5ca675",
".cursor/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "6e2d6d88719c2779fe34004f63d36cff203d8f64e7fb620f7cb1cde15c37c462",
".cursor/skills/trellis-meta/references/platform-files/overview.md": "6479cd2393166b4b369b511c44b78cbc64975c8b1df96ee1d4d1bd06b75cd48d",
".cursor/skills/trellis-meta/references/platform-files/platform-map.md": "ded6751c06f31d0a701d33c9dd69c482a583539ad3ed464aaad9e705f793b212",
".cursor/skills/trellis-meta/references/platform-files/skills-and-commands.md": "85435eb8bb6921283575bca51268fc534c22fd3ca33782e841ee5c76140ae48f",
".cursor/skills/trellis-meta/SKILL.md": "942e898a6fd769a93a3ca6f43f9fe0412d0adae011654fd384e9cacbd2af4f34",
".cursor/skills/trellis-update-spec/SKILL.md": "cef32aec88db973a0a0272cf18b91d4585fe6ed6625e3de06851a5d3402f65d6"
}
}

1
.trellis/.version Normal file
View File

@@ -0,0 +1 @@
0.5.4

59
.trellis/config.yaml Normal file
View File

@@ -0,0 +1,59 @@
# Trellis Configuration
# Project-level settings for the Trellis workflow system
#
# All values have sensible defaults. Only override what you need.
#-------------------------------------------------------------------------------
# Session Recording
#-------------------------------------------------------------------------------
# Commit message used when auto-committing journal/index changes
# after running add_session.py
session_commit_message: "chore: record journal"
# Maximum lines per journal file before rotating to a new one
max_journal_lines: 2000
#-------------------------------------------------------------------------------
# Task Lifecycle Hooks
#-------------------------------------------------------------------------------
# Shell commands to run after task lifecycle events.
# Each hook receives TASK_JSON_PATH environment variable pointing to task.json.
# Hook failures print a warning but do not block the main operation.
#
# hooks:
# after_create:
# - "echo 'Task created'"
# after_start:
# - "echo 'Task started'"
# after_finish:
# - "echo 'Task finished'"
# after_archive:
# - "echo 'Task archived'"
#-------------------------------------------------------------------------------
# Monorepo / Packages
#-------------------------------------------------------------------------------
# Declare packages for monorepo projects.
# Trellis auto-detects workspaces during `trellis init`, but you can also
# configure them manually here.
#
# packages:
# frontend:
# path: packages/frontend
# backend:
# path: packages/backend
# docs:
# path: docs-site
# type: submodule
# # For polyrepo / meta-repo layouts (independent .git in each subdir),
# # mark the package with `git: true`. The runtime treats it as an
# # independent repository for things like git-context display.
# webapp:
# path: ./webapp
# git: true
# Default package used when --package is not specified.
# default_package: frontend

View File

@@ -0,0 +1,5 @@
"""
Trellis Python Scripts
This module provides Python implementations of Trellis workflow scripts.
"""

View File

@@ -0,0 +1,521 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Add a new session to journal file and update index.md.
Usage:
python add_session.py --title "Title" --commit "hash" --summary "Summary" [--package cli]
python add_session.py --title "Title" --branch "feat/my-branch"
# Pipe detailed content via stdin (use --stdin to opt in):
cat << 'EOF' | python add_session.py --stdin --title "Title" --summary "Summary"
<session content here>
EOF
Branch resolution order:
1. --branch CLI arg (explicit)
2. task.json branch field (from active task)
3. git branch --show-current (auto-detect)
4. None (omitted gracefully)
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from common.paths import (
FILE_JOURNAL_PREFIX,
get_repo_root,
get_current_task,
get_developer,
get_workspace_dir,
)
from common.developer import ensure_developer
from common.git import run_git
from common.tasks import load_task
from common.config import (
get_packages,
get_session_commit_message,
get_max_journal_lines,
is_monorepo,
resolve_package,
validate_package,
)
# =============================================================================
# Helper Functions
# =============================================================================
def get_latest_journal_info(dev_dir: Path) -> tuple[Path | None, int, int]:
"""Get latest journal file info.
Returns:
Tuple of (file_path, file_number, line_count).
"""
latest_file: Path | None = None
latest_num = -1
for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
if not f.is_file():
continue
match = re.search(r"(\d+)$", f.stem)
if match:
num = int(match.group(1))
if num > latest_num:
latest_num = num
latest_file = f
if latest_file:
lines = len(latest_file.read_text(encoding="utf-8").splitlines())
return latest_file, latest_num, lines
return None, 0, 0
def get_current_session(index_file: Path) -> int:
"""Get current session number from index.md."""
if not index_file.is_file():
return 0
content = index_file.read_text(encoding="utf-8")
for line in content.splitlines():
if "Total Sessions" in line:
match = re.search(r":\s*(\d+)", line)
if match:
return int(match.group(1))
return 0
def _extract_journal_num(filename: str) -> int:
"""Extract journal number from filename for sorting."""
match = re.search(r"(\d+)", filename)
return int(match.group(1)) if match else 0
def count_journal_files(dev_dir: Path, active_num: int) -> str:
"""Count journal files and return table rows."""
active_file = f"{FILE_JOURNAL_PREFIX}{active_num}.md"
result_lines = []
files = sorted(
[f for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md") if f.is_file()],
key=lambda f: _extract_journal_num(f.stem),
reverse=True
)
for f in files:
filename = f.name
lines = len(f.read_text(encoding="utf-8").splitlines())
status = "Active" if filename == active_file else "Archived"
result_lines.append(f"| `{filename}` | ~{lines} | {status} |")
return "\n".join(result_lines)
def create_new_journal_file(
dev_dir: Path, num: int, developer: str, today: str, max_lines: int = 2000,
) -> Path:
"""Create a new journal file."""
prev_num = num - 1
new_file = dev_dir / f"{FILE_JOURNAL_PREFIX}{num}.md"
content = f"""# Journal - {developer} (Part {num})
> Continuation from `{FILE_JOURNAL_PREFIX}{prev_num}.md` (archived at ~{max_lines} lines)
> Started: {today}
---
"""
new_file.write_text(content, encoding="utf-8")
return new_file
def generate_session_content(
session_num: int,
title: str,
commit: str,
summary: str,
extra_content: str,
today: str,
package: str | None = None,
branch: str | None = None,
) -> str:
"""Generate session content."""
if commit and commit != "-":
commit_table = """| Hash | Message |
|------|---------|"""
for c in commit.split(","):
c = c.strip()
commit_table += f"\n| `{c}` | (see git log) |"
else:
commit_table = "(No commits - planning session)"
package_line = f"\n**Package**: {package}" if package else ""
branch_line = f"\n**Branch**: `{branch}`" if branch else ""
return f"""
## Session {session_num}: {title}
**Date**: {today}
**Task**: {title}{package_line}{branch_line}
### Summary
{summary}
### Main Changes
{extra_content}
### Git Commits
{commit_table}
### Testing
- [OK] (Add test results)
### Status
[OK] **Completed**
### Next Steps
- None - task complete
"""
def update_index(
index_file: Path,
dev_dir: Path,
title: str,
commit: str,
new_session: int,
active_file: str,
today: str,
branch: str | None = None,
) -> bool:
"""Update index.md with new session info."""
# Format commit for display
commit_display = "-"
if commit and commit != "-":
commit_display = re.sub(r"([a-f0-9]{7,})", r"`\1`", commit.replace(",", ", "))
# Get file number from active_file name
match = re.search(r"(\d+)", active_file)
active_num = int(match.group(1)) if match else 0
files_table = count_journal_files(dev_dir, active_num)
print(f"Updating index.md for session {new_session}...")
print(f" Title: {title}")
print(f" Commit: {commit_display}")
print(f" Active File: {active_file}")
print()
content = index_file.read_text(encoding="utf-8")
if "@@@auto:current-status" not in content:
print("Error: Markers not found in index.md. Please ensure markers exist.", file=sys.stderr)
return False
# Process sections
lines = content.splitlines()
new_lines = []
in_current_status = False
in_active_documents = False
in_session_history = False
header_written = False
for line in lines:
if "@@@auto:current-status" in line:
new_lines.append(line)
in_current_status = True
new_lines.append(f"- **Active File**: `{active_file}`")
new_lines.append(f"- **Total Sessions**: {new_session}")
new_lines.append(f"- **Last Active**: {today}")
continue
if "@@@/auto:current-status" in line:
in_current_status = False
new_lines.append(line)
continue
if "@@@auto:active-documents" in line:
new_lines.append(line)
in_active_documents = True
new_lines.append("| File | Lines | Status |")
new_lines.append("|------|-------|--------|")
new_lines.append(files_table)
continue
if "@@@/auto:active-documents" in line:
in_active_documents = False
new_lines.append(line)
continue
if "@@@auto:session-history" in line:
new_lines.append(line)
in_session_history = True
header_written = False
continue
if "@@@/auto:session-history" in line:
in_session_history = False
new_lines.append(line)
continue
if in_current_status:
continue
if in_active_documents:
continue
if in_session_history:
# Migrate old 4/6-column headers to 5-column Branch-only history.
if re.match(
r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*Base Branch\s*\|\s*$",
line,
):
new_lines.append("| # | Date | Title | Commits | Branch |")
continue
if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*$", line):
new_lines.append("| # | Date | Title | Commits | Branch |")
continue
if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*$", line):
new_lines.append("| # | Date | Title | Commits | Branch |")
continue
if re.match(r"^\|[-| ]+\|\s*$", line) and not header_written:
new_lines.append("|---|------|-------|---------|--------|")
new_lines.append(f"| {new_session} | {today} | {title} | {commit_display} | `{branch or '-'}` |")
header_written = True
continue
new_lines.append(line)
continue
new_lines.append(line)
index_file.write_text("\n".join(new_lines), encoding="utf-8")
print("[OK] Updated index.md successfully!")
return True
# =============================================================================
# Main Function
# =============================================================================
def _auto_commit_workspace(repo_root: Path) -> None:
"""Stage .trellis/workspace and .trellis/tasks, then commit with a configured message."""
commit_msg = get_session_commit_message(repo_root)
add_result = subprocess.run(
["git", "add", "-A", ".trellis/workspace", ".trellis/tasks"],
cwd=repo_root,
capture_output=True,
text=True,
)
if add_result.returncode != 0:
print(f"[WARN] git add failed (exit {add_result.returncode}): {add_result.stderr.strip()}", file=sys.stderr)
print("[WARN] Please commit .trellis/ changes manually: git add .trellis && git commit", file=sys.stderr)
return
# Check if there are staged changes
result = subprocess.run(
["git", "diff", "--cached", "--quiet", "--", ".trellis/workspace", ".trellis/tasks"],
cwd=repo_root,
)
if result.returncode == 0:
print("[OK] No workspace changes to commit.", file=sys.stderr)
return
commit_result = subprocess.run(
["git", "commit", "-m", commit_msg],
cwd=repo_root,
capture_output=True,
text=True,
)
if commit_result.returncode == 0:
print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
else:
print(f"[WARN] Auto-commit failed: {commit_result.stderr.strip()}", file=sys.stderr)
def add_session(
title: str,
commit: str = "-",
summary: str = "(Add summary)",
extra_content: str = "(Add details)",
auto_commit: bool = True,
package: str | None = None,
branch: str | None = None,
) -> int:
"""Add a new session."""
repo_root = get_repo_root()
ensure_developer(repo_root)
developer = get_developer(repo_root)
if not developer:
print("Error: Developer not initialized", file=sys.stderr)
return 1
dev_dir = get_workspace_dir(repo_root)
if not dev_dir:
print("Error: Workspace directory not found", file=sys.stderr)
return 1
max_lines = get_max_journal_lines(repo_root)
index_file = dev_dir / "index.md"
today = datetime.now().strftime("%Y-%m-%d")
journal_file, current_num, current_lines = get_latest_journal_info(dev_dir)
current_session = get_current_session(index_file)
new_session = current_session + 1
session_content = generate_session_content(
new_session, title, commit, summary, extra_content, today, package,
branch,
)
content_lines = len(session_content.splitlines())
print("========================================", file=sys.stderr)
print("ADD SESSION", file=sys.stderr)
print("========================================", file=sys.stderr)
print("", file=sys.stderr)
print(f"Session: {new_session}", file=sys.stderr)
print(f"Title: {title}", file=sys.stderr)
print(f"Commit: {commit}", file=sys.stderr)
print("", file=sys.stderr)
print(f"Current journal file: {FILE_JOURNAL_PREFIX}{current_num}.md", file=sys.stderr)
print(f"Current lines: {current_lines}", file=sys.stderr)
print(f"New content lines: {content_lines}", file=sys.stderr)
print(f"Total after append: {current_lines + content_lines}", file=sys.stderr)
print("", file=sys.stderr)
target_file = journal_file
target_num = current_num
if current_lines + content_lines > max_lines:
target_num = current_num + 1
print(f"[!] Exceeds {max_lines} lines, creating {FILE_JOURNAL_PREFIX}{target_num}.md", file=sys.stderr)
target_file = create_new_journal_file(dev_dir, target_num, developer, today, max_lines)
print(f"Created: {target_file}", file=sys.stderr)
# Append session content
if target_file:
with target_file.open("a", encoding="utf-8") as f:
f.write(session_content)
print(f"[OK] Appended session to {target_file.name}", file=sys.stderr)
print("", file=sys.stderr)
# Update index.md
active_file = f"{FILE_JOURNAL_PREFIX}{target_num}.md"
if not update_index(
index_file,
dev_dir,
title,
commit,
new_session,
active_file,
today,
branch,
):
return 1
print("", file=sys.stderr)
print("========================================", file=sys.stderr)
print(f"[OK] Session {new_session} added successfully!", file=sys.stderr)
print("========================================", file=sys.stderr)
print("", file=sys.stderr)
print("Files updated:", file=sys.stderr)
print(f" - {target_file.name if target_file else 'journal'}", file=sys.stderr)
print(" - index.md", file=sys.stderr)
# Auto-commit workspace changes
if auto_commit:
print("", file=sys.stderr)
_auto_commit_workspace(repo_root)
return 0
# =============================================================================
# Main Entry
# =============================================================================
def main() -> int:
"""CLI entry point."""
parser = argparse.ArgumentParser(
description="Add a new session to journal file and update index.md"
)
parser.add_argument("--title", required=True, help="Session title")
parser.add_argument("--commit", default="-", help="Comma-separated commit hashes")
parser.add_argument("--summary", default="(Add summary)", help="Brief summary")
parser.add_argument("--content-file", help="Path to file with detailed content")
parser.add_argument("--package", help="Package name tag (e.g., cli, docs-site)")
parser.add_argument("--branch", help="Branch name (auto-detected if omitted)")
parser.add_argument("--no-commit", action="store_true",
help="Skip auto-commit of workspace changes")
parser.add_argument("--stdin", action="store_true",
help="Read extra content from stdin (explicit opt-in)")
args = parser.parse_args()
extra_content = "(Add details)"
if args.content_file:
content_path = Path(args.content_file)
if content_path.is_file():
extra_content = content_path.read_text(encoding="utf-8")
elif args.stdin:
extra_content = sys.stdin.read()
# Load active task once — shared by package and branch resolution
repo_root = get_repo_root()
current = get_current_task(repo_root)
task_data = load_task(repo_root / current) if current else None
package = args.package
if package:
# CLI source: fail-fast in monorepo, ignore in single-repo
if not is_monorepo(repo_root):
print("Warning: --package ignored in single-repo project", file=sys.stderr)
package = None
elif not validate_package(package, repo_root):
packages = get_packages(repo_root)
available = ", ".join(sorted(packages.keys())) if packages else "(none)"
print(f"Error: unknown package '{package}'. Available: {available}", file=sys.stderr)
return 1
else:
# Inferred: active task's task.json.package → default_package → None
task_package = task_data.package if task_data else None
package = resolve_package(task_package, repo_root)
# Resolve branch: CLI → task.json → git auto-detect → None
branch = args.branch
if not branch:
if task_data and task_data.raw.get("branch"):
branch = task_data.raw["branch"]
else:
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
detected = branch_out.strip()
if detected:
branch = detected
return add_session(
args.title, args.commit, args.summary, extra_content,
auto_commit=not args.no_commit,
package=package,
branch=branch,
)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,92 @@
"""
Common utilities for Trellis workflow scripts.
This module provides shared functionality used by other Trellis scripts.
"""
import io
import sys
# =============================================================================
# Windows Encoding Fix (MUST be at top, before any other output)
# =============================================================================
# On Windows, stdout defaults to the system code page (often GBK/CP936).
# This causes UnicodeEncodeError when printing non-ASCII characters.
#
# Any script that imports from common will automatically get this fix.
# =============================================================================
def _configure_stream(stream: object) -> object:
"""Configure a stream for UTF-8 encoding on Windows."""
# Try reconfigure() first (Python 3.7+, more reliable)
if hasattr(stream, "reconfigure"):
stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
return stream
# Fallback: detach and rewrap with TextIOWrapper
elif hasattr(stream, "detach"):
return io.TextIOWrapper(
stream.detach(), # type: ignore[union-attr]
encoding="utf-8",
errors="replace",
)
return stream
if sys.platform == "win32":
sys.stdout = _configure_stream(sys.stdout) # type: ignore[assignment]
sys.stderr = _configure_stream(sys.stderr) # type: ignore[assignment]
sys.stdin = _configure_stream(sys.stdin) # type: ignore[assignment]
def configure_encoding() -> None:
"""
Configure stdout/stderr/stdin for UTF-8 encoding on Windows.
This is automatically called when importing from common,
but can be called manually for scripts that don't import common.
Safe to call multiple times.
"""
global sys
if sys.platform == "win32":
sys.stdout = _configure_stream(sys.stdout) # type: ignore[assignment]
sys.stderr = _configure_stream(sys.stderr) # type: ignore[assignment]
sys.stdin = _configure_stream(sys.stdin) # type: ignore[assignment]
from .paths import (
DIR_WORKFLOW,
DIR_WORKSPACE,
DIR_TASKS,
DIR_ARCHIVE,
DIR_SPEC,
DIR_SCRIPTS,
FILE_DEVELOPER,
FILE_CURRENT_TASK,
FILE_TASK_JSON,
FILE_JOURNAL_PREFIX,
get_repo_root,
get_developer,
check_developer,
get_tasks_dir,
get_workspace_dir,
get_active_journal_file,
count_lines,
get_current_task,
get_current_task_abs,
normalize_task_ref,
resolve_task_ref,
set_current_task,
clear_current_task,
has_current_task,
generate_task_date_prefix,
)
from .active_task import (
ActiveTask,
clear_active_task,
resolve_active_task,
resolve_context_key,
set_active_task,
)

View File

@@ -0,0 +1,626 @@
#!/usr/bin/env python3
"""Session-scoped active task resolution.
The user-facing concept is a single "active task". Trellis stores that pointer
per AI session/window under `.trellis/.runtime/sessions/`; without a stable
session key there is no active task.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import sys
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
DIR_WORKFLOW = ".trellis"
DIR_TASKS = "tasks"
DIR_RUNTIME = ".runtime"
DIR_SESSIONS = "sessions"
DIR_CURSOR_SHELL = "cursor-shell"
CURSOR_SHELL_TICKET_TTL_SECONDS = 30
TASK_SESSION_COMMANDS = {"start", "current", "finish"}
_SESSION_KEYS = ("session_id", "sessionId", "sessionID")
_CONVERSATION_KEYS = ("conversation_id", "conversationId", "conversationID")
_TRANSCRIPT_KEYS = ("transcript_path", "transcriptPath", "transcript")
_NESTED_KEYS = ("input", "properties", "event", "hook_input", "hookInput")
_KNOWN_PLATFORMS = {
"claude",
"codex",
"cursor",
"opencode",
"gemini",
"droid",
"qoder",
"codebuddy",
"kiro",
"copilot",
"pi",
}
_ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = (
("claude", ("CLAUDE_SESSION_ID", "CLAUDE_CODE_SESSION_ID")),
("codex", ("CODEX_SESSION_ID", "CODEX_THREAD_ID")),
("cursor", ("CURSOR_SESSION_ID",)),
("opencode", ("OPENCODE_SESSION_ID", "OPENCODE_SESSIONID", "OPENCODE_RUN_ID")),
("gemini", ("GEMINI_SESSION_ID",)),
("droid", ("FACTORY_SESSION_ID", "DROID_SESSION_ID")),
("qoder", ("QODER_SESSION_ID",)),
("codebuddy", ("CODEBUDDY_SESSION_ID",)),
("kiro", ("KIRO_SESSION_ID",)),
("copilot", ("COPILOT_SESSION_ID", "COPILOT_SESSIONID")),
("pi", ("PI_SESSION_ID", "PI_SESSIONID")),
)
_ENV_CONVERSATION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = (
("cursor", ("CURSOR_CONVERSATION_ID", "CURSOR_CONVERSATIONID")),
)
_ENV_TRANSCRIPT_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = (
("claude", ("CLAUDE_TRANSCRIPT_PATH",)),
("codex", ("CODEX_TRANSCRIPT_PATH",)),
("cursor", ("CURSOR_TRANSCRIPT_PATH",)),
("gemini", ("GEMINI_TRANSCRIPT_PATH",)),
("droid", ("FACTORY_TRANSCRIPT_PATH", "DROID_TRANSCRIPT_PATH")),
("qoder", ("QODER_TRANSCRIPT_PATH",)),
("codebuddy", ("CODEBUDDY_TRANSCRIPT_PATH",)),
)
_ENV_PLATFORM_ALIASES = {
"claude-code": "claude",
"factory": "droid",
"factory-ai": "droid",
"github-copilot": "copilot",
}
@dataclass(frozen=True)
class ActiveTask:
"""Resolved active task state."""
task_path: str | None
source_type: str
context_key: str | None = None
stale: bool = False
@property
def source(self) -> str:
"""Human-readable source label."""
if self.source_type == "session" and self.context_key:
return f"session:{self.context_key}"
if self.source_type == "session-fallback" and self.context_key:
return f"session-fallback:{self.context_key}"
return self.source_type
def normalize_task_ref(task_ref: str) -> str:
"""Normalize a task ref for stable storage and comparison."""
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(f"{DIR_TASKS}/"):
return f"{DIR_WORKFLOW}/{normalized}"
return normalized
def resolve_task_ref(task_ref: str, repo_root: Path) -> Path | None:
"""Resolve a task ref to an absolute task directory."""
normalized = normalize_task_ref(task_ref)
if not normalized:
return None
path_obj = Path(normalized)
if path_obj.is_absolute():
return path_obj
if normalized.startswith(f"{DIR_WORKFLOW}/"):
return repo_root / path_obj
return repo_root / DIR_WORKFLOW / DIR_TASKS / path_obj
def _runtime_sessions_dir(repo_root: Path) -> Path:
return repo_root / DIR_WORKFLOW / DIR_RUNTIME / DIR_SESSIONS
def _sanitize_key(raw: str) -> str:
safe = re.sub(r"[^A-Za-z0-9._-]+", "_", raw.strip())
safe = safe.strip("._-")
return safe[:160] if safe else ""
def _hash_value(raw: str) -> str:
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]
def _as_dict(value: Any) -> dict[str, Any] | None:
return value if isinstance(value, dict) else None
def _string_value(value: Any) -> str | None:
if isinstance(value, str):
stripped = value.strip()
return stripped or None
return None
def _lookup_string(data: dict[str, Any], keys: tuple[str, ...]) -> str | None:
for key in keys:
value = _string_value(data.get(key))
if value:
return value
for nested_key in _NESTED_KEYS:
nested = _as_dict(data.get(nested_key))
if not nested:
continue
value = _lookup_string(nested, keys)
if value:
return value
return None
def _detect_platform(platform_input: dict[str, Any] | None, platform: str | None) -> str:
if platform:
return _sanitize_key(platform) or "session"
if platform_input:
for key in ("_trellis_platform", "trellis_platform", "platform", "source"):
value = _string_value(platform_input.get(key))
if value:
return _sanitize_key(value) or "session"
if _string_value(platform_input.get("cursor_version")):
return "cursor"
return "session"
def _context_key(platform_name: str, kind: str, value: str) -> str:
if kind == "transcript":
return f"{platform_name}_transcript_{_hash_value(value)}"
safe_value = _sanitize_key(value)
if safe_value:
return f"{platform_name}_{safe_value}"
return f"{platform_name}_{_hash_value(value)}"
def _iter_env_keys(
env_keys: tuple[tuple[str, tuple[str, ...]], ...],
platform_name: str | None,
) -> tuple[tuple[str, tuple[str, ...]], ...]:
if not platform_name:
return env_keys
matched = tuple((name, keys) for name, keys in env_keys if name == platform_name)
return matched
def _env_platform_name(platform_name: str | None) -> str | None:
if not platform_name or platform_name == "session":
return None
return _ENV_PLATFORM_ALIASES.get(platform_name, platform_name)
def _lookup_env_context_key(platform_name: str | None) -> str | None:
"""Resolve a context key from platform-provided environment variables.
Hooks pass `TRELLIS_CONTEXT_ID` to subprocesses they launch, but an AI-run
shell command can only see session identity if the host platform exports it
in the command environment. These names are best-effort adapters; if none
are present, there is no session-scoped active task.
"""
env_platform_name = _env_platform_name(platform_name)
for name, keys in _iter_env_keys(_ENV_SESSION_KEYS, env_platform_name):
for key in keys:
value = _string_value(os.environ.get(key))
if value:
return _context_key(name, "session", value)
for name, keys in _iter_env_keys(_ENV_CONVERSATION_KEYS, env_platform_name):
for key in keys:
value = _string_value(os.environ.get(key))
if value:
return _context_key(name, "conversation", value)
for name, keys in _iter_env_keys(_ENV_TRANSCRIPT_KEYS, env_platform_name):
for key in keys:
value = _string_value(os.environ.get(key))
if value:
return _context_key(name, "transcript", value)
return None
def _find_repo_root_from_cwd() -> Path | None:
current = Path.cwd().resolve()
while True:
if (current / DIR_WORKFLOW).is_dir():
return current
if current == current.parent:
return None
current = current.parent
def _cursor_shell_ticket_dir(repo_root: Path) -> Path:
return repo_root / DIR_WORKFLOW / DIR_RUNTIME / DIR_CURSOR_SHELL
def _remove_file(path: Path) -> bool:
try:
path.unlink()
return True
except OSError:
return False
def _task_refs_match(left: str | None, right: str | None, repo_root: Path) -> bool:
if not left or not right:
return False
left_path = resolve_task_ref(left, repo_root)
right_path = resolve_task_ref(right, repo_root)
if left_path is not None and right_path is not None:
return left_path == right_path
return normalize_task_ref(left) == normalize_task_ref(right)
def _pending_ticket_matches_args(ticket: dict[str, Any], repo_root: Path) -> bool:
if Path(sys.argv[0]).name != "task.py":
return False
args = tuple(sys.argv[1:])
if not args:
return False
command_name = args[0]
if command_name not in TASK_SESSION_COMMANDS:
return False
subcommands = ticket.get("subcommands")
if not isinstance(subcommands, list):
return False
for subcommand in subcommands:
if not isinstance(subcommand, dict):
continue
if _string_value(subcommand.get("name")) != command_name:
continue
if command_name != "start":
return True
task_ref = args[1] if len(args) > 1 else None
if _task_refs_match(_string_value(subcommand.get("task_ref")), task_ref, repo_root):
return True
return False
def _ticket_is_fresh(ticket: dict[str, Any], ticket_path: Path, now: float) -> bool:
expires_at = ticket.get("expires_at_epoch")
if isinstance(expires_at, (int, float)) and expires_at < now:
_remove_file(ticket_path)
return False
created_at = ticket.get("created_at_epoch")
if isinstance(created_at, (int, float)):
if now - created_at <= CURSOR_SHELL_TICKET_TTL_SECONDS:
return True
_remove_file(ticket_path)
return False
return True
def _ticket_cwd_matches_repo(ticket: dict[str, Any], repo_root: Path) -> bool:
cwd = _string_value(ticket.get("cwd"))
if not cwd:
return True
try:
Path(cwd).resolve().relative_to(repo_root)
except ValueError:
return False
return True
def _matching_cursor_ticket_context_key(
ticket_path: Path,
repo_root: Path,
now: float,
) -> str | None:
ticket = _read_json(ticket_path)
if ticket is None or ticket.get("platform") != "cursor":
return None
if not _ticket_is_fresh(ticket, ticket_path, now):
return None
if not _ticket_cwd_matches_repo(ticket, repo_root):
return None
if not _pending_ticket_matches_args(ticket, repo_root):
return None
return _string_value(ticket.get("context_key"))
def _lookup_cursor_shell_ticket_context_key() -> str | None:
"""Resolve Cursor conversation identity from a short-lived shell ticket.
Cursor exposes `conversation_id` to `beforeShellExecution`, but does not
export it into the shell command environment. The Cursor hook writes a
short-lived ticket just before `task.py` runs. We accept a ticket only when
the current `task.py` subcommand matches and exactly one fresh context key
matches, which avoids cross-window pointer contamination.
"""
repo_root = _find_repo_root_from_cwd()
if repo_root is None:
return None
ticket_dir = _cursor_shell_ticket_dir(repo_root)
if not ticket_dir.is_dir():
return None
now = time.time()
candidates: set[str] = set()
for ticket_path in ticket_dir.glob("*.json"):
context_key = _matching_cursor_ticket_context_key(ticket_path, repo_root, now)
if context_key:
candidates.add(context_key)
if len(candidates) == 1:
return next(iter(candidates))
return None
def resolve_context_key(
platform_input: dict[str, Any] | None = None,
platform: str | None = None,
) -> str | None:
"""Resolve a stable session/window context key, if one is available.
`TRELLIS_CONTEXT_ID` is an explicit context-key override used by CLI
scripts and subprocesses. It does not store the task itself.
"""
override = _string_value(os.environ.get("TRELLIS_CONTEXT_ID"))
if override:
return _sanitize_key(override) or _hash_value(override)
data = _as_dict(platform_input)
platform_name = _detect_platform(data, platform) if data or platform else None
if data:
session_id = _lookup_string(data, _SESSION_KEYS)
if session_id:
return _context_key(platform_name or "session", "session", session_id)
conversation_id = _lookup_string(data, _CONVERSATION_KEYS)
if conversation_id:
return _context_key(platform_name or "session", "conversation", conversation_id)
transcript_path = _lookup_string(data, _TRANSCRIPT_KEYS)
if transcript_path:
return _context_key(platform_name or "session", "transcript", transcript_path)
env_context_key = _lookup_env_context_key(platform_name)
if env_context_key:
return env_context_key
if platform_name in (None, "session", "cursor"):
return _lookup_cursor_shell_ticket_context_key()
return None
def _read_json(path: Path) -> dict[str, Any] | None:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError, OSError):
return None
return data if isinstance(data, dict) else None
def _write_json(path: Path, data: dict[str, Any]) -> bool:
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return True
except OSError:
return False
def _canonical_task_ref(task_path: str, repo_root: Path) -> str | None:
normalized = normalize_task_ref(task_path)
if not normalized:
return None
full_path = resolve_task_ref(normalized, repo_root)
if full_path is None or not full_path.is_dir():
return None
try:
return full_path.relative_to(repo_root).as_posix()
except ValueError:
return str(full_path)
def _active_from_ref(
task_ref: str | None,
repo_root: Path,
source_type: str,
context_key: str | None = None,
) -> ActiveTask | None:
if not task_ref:
return None
resolved = resolve_task_ref(task_ref, repo_root)
stale = resolved is None or not resolved.is_dir()
return ActiveTask(task_ref, source_type, context_key, stale)
def _context_path(repo_root: Path, context_key: str) -> Path:
return _runtime_sessions_dir(repo_root) / f"{context_key}.json"
def resolve_active_task(
repo_root: Path,
platform_input: dict[str, Any] | None = None,
platform: str | None = None,
) -> ActiveTask:
"""Resolve the active task from session runtime state only.
A stale session task is returned as stale. Missing context identity or a
missing/empty session context falls back to single-session inference: if
exactly one session file exists in the runtime, return its task with
source_type="session-fallback" — covers class-2 platform sub-agents (codex,
copilot, gemini, qoder) that don't inherit the parent's session id. ≥2
files or 0 files yield ActiveTask(None) — refuses to guess across windows.
"""
context_key = resolve_context_key(platform_input, platform)
if context_key:
context = _read_json(_context_path(repo_root, context_key)) or {}
task_ref = _string_value(context.get("current_task"))
active = _active_from_ref(task_ref, repo_root, "session", context_key)
if active:
return active
fallback = _resolve_single_session_fallback(repo_root)
if fallback is not None:
return fallback
return ActiveTask(None, "none", context_key)
def _resolve_single_session_fallback(repo_root: Path) -> ActiveTask | None:
"""Return the task pointed at by the sole session file, if exactly one exists.
Used when context-key resolution fails (typical for class-2 platform
sub-agents). Returns None if 0 or ≥2 session files are present — refuses
to pick across windows so 04-21's multi-session isolation contract holds.
"""
sessions_dir = _runtime_sessions_dir(repo_root)
if not sessions_dir.is_dir():
return None
session_files = sorted(sessions_dir.glob("*.json"))
if len(session_files) != 1:
return None
session_file = session_files[0]
context = _read_json(session_file) or {}
task_ref = _string_value(context.get("current_task"))
if not task_ref:
return None
fallback_key = session_file.stem
return _active_from_ref(task_ref, repo_root, "session-fallback", fallback_key)
def _utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def _context_metadata(
platform_input: dict[str, Any] | None,
platform: str | None,
context_key: str | None = None,
) -> dict[str, Any]:
data = _as_dict(platform_input) or {}
platform_name = _detect_platform(data, platform)
if platform_name == "session" and context_key:
prefix = context_key.split("_", 1)[0]
if prefix in _KNOWN_PLATFORMS:
platform_name = prefix
metadata: dict[str, Any] = {
"platform": platform_name,
"last_seen_at": _utc_now(),
}
for key in (*_SESSION_KEYS, *_CONVERSATION_KEYS, *_TRANSCRIPT_KEYS):
value = _lookup_string(data, (key,))
if value:
metadata[key] = value
return metadata
def set_active_task(
task_path: str,
repo_root: Path,
platform_input: dict[str, Any] | None = None,
platform: str | None = None,
) -> ActiveTask | None:
"""Set the active task in session scope.
Returns None when no context key is available; callers should surface a
user-facing error that explains how to provide session identity.
"""
canonical = _canonical_task_ref(task_path, repo_root)
if canonical is None:
return None
context_key = resolve_context_key(platform_input, platform)
if not context_key:
return None
context_path = _context_path(repo_root, context_key)
context = _read_json(context_path) or {}
context.update(_context_metadata(platform_input, platform, context_key))
context["current_task"] = canonical
context.setdefault("current_run", None)
if not _write_json(context_path, context):
return None
return ActiveTask(canonical, "session", context_key)
def clear_active_task(
repo_root: Path,
platform_input: dict[str, Any] | None = None,
platform: str | None = None,
) -> ActiveTask:
"""Clear the active task by deleting the current session context file."""
context_key = resolve_context_key(platform_input, platform)
if not context_key:
return ActiveTask(None, "none")
previous = resolve_active_task(repo_root, platform_input, platform)
context_path = _context_path(repo_root, context_key)
if context_path.is_file():
_remove_file(context_path)
return previous
def clear_task_from_sessions(task_path: str, repo_root: Path) -> int:
"""Delete all session runtime files that point at a task."""
target = _canonical_task_ref(task_path, repo_root) or normalize_task_ref(task_path)
if not target:
return 0
cleared = 0
sessions_dir = _runtime_sessions_dir(repo_root)
if not sessions_dir.is_dir():
return cleared
for session_path in sessions_dir.glob("*.json"):
context = _read_json(session_path) or {}
current = _string_value(context.get("current_task"))
if not current:
continue
current_ref = _canonical_task_ref(current, repo_root) or normalize_task_ref(current)
if current_ref != target:
continue
if session_path.is_file() and _remove_file(session_path):
cleared += 1
return cleared
def get_current_task_source(
repo_root: Path,
platform_input: dict[str, Any] | None = None,
platform: str | None = None,
) -> tuple[str, str | None, str | None]:
"""Return (`source_type`, `context_key`, `task_path`) for compatibility."""
active = resolve_active_task(repo_root, platform_input, platform)
return active.source_type, active.context_key, active.task_path

View File

@@ -0,0 +1,811 @@
"""
CLI Adapter for Multi-Platform Support.
Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Windsurf, Qoder, CodeBuddy, GitHub Copilot, Factory Droid, and Pi Agent interfaces.
Supported platforms:
- claude: Claude Code (default)
- opencode: OpenCode
- cursor: Cursor IDE
- iflow: iFlow CLI
- codex: Codex CLI (skills-based)
- kilo: Kilo CLI
- kiro: Kiro Code (skills-based)
- gemini: Gemini CLI
- antigravity: Antigravity (workflow-based)
- windsurf: Windsurf (workflow-based)
- qoder: Qoder
- codebuddy: CodeBuddy
- copilot: GitHub Copilot (VS Code)
- droid: Factory Droid (commands-based)
- pi: Pi Agent (extension-backed)
Usage:
from common.cli_adapter import CLIAdapter
adapter = CLIAdapter("opencode")
cmd = adapter.build_run_command(
agent="dispatch",
session_id="abc123",
prompt="Start the pipeline"
)
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import ClassVar, Literal
Platform = Literal[
"claude",
"opencode",
"cursor",
"iflow",
"codex",
"kilo",
"kiro",
"gemini",
"antigravity",
"windsurf",
"qoder",
"codebuddy",
"copilot",
"droid",
"pi",
]
@dataclass
class CLIAdapter:
"""Adapter for different AI coding CLI tools."""
platform: Platform
# =========================================================================
# Agent Name Mapping
# =========================================================================
# OpenCode has built-in agents that cannot be overridden
# See: https://github.com/sst/opencode/issues/4271
# Note: Class-level constant, not a dataclass field
_AGENT_NAME_MAP: ClassVar[dict[Platform, dict[str, str]]] = {
"claude": {}, # No mapping needed
"opencode": {
"plan": "trellis-plan", # 'plan' is built-in in OpenCode
},
}
def get_agent_name(self, agent: str) -> str:
"""Get platform-specific agent name.
Args:
agent: Original agent name (e.g., 'plan', 'dispatch')
Returns:
Platform-specific agent name (e.g., 'trellis-plan' for OpenCode)
"""
mapping = self._AGENT_NAME_MAP.get(self.platform, {})
return mapping.get(agent, agent)
# =========================================================================
# Agent Path
# =========================================================================
@property
def config_dir_name(self) -> str:
"""Get platform-specific config directory name.
Returns:
Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.windsurf', '.qoder', '.codebuddy', '.github/copilot', '.factory', or '.pi')
"""
if self.platform == "opencode":
return ".opencode"
elif self.platform == "cursor":
return ".cursor"
elif self.platform == "iflow":
return ".iflow"
elif self.platform == "codex":
return ".codex"
elif self.platform == "kilo":
return ".kilocode"
elif self.platform == "kiro":
return ".kiro"
elif self.platform == "gemini":
return ".gemini"
elif self.platform == "antigravity":
return ".agent"
elif self.platform == "windsurf":
return ".windsurf"
elif self.platform == "qoder":
return ".qoder"
elif self.platform == "codebuddy":
return ".codebuddy"
elif self.platform == "copilot":
return ".github/copilot"
elif self.platform == "droid":
return ".factory"
elif self.platform == "pi":
return ".pi"
else:
return ".claude"
def get_config_dir(self, project_root: Path) -> Path:
"""Get platform-specific config directory.
Args:
project_root: Project root directory
Returns:
Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .windsurf, .qoder, .codebuddy, .github/copilot, .factory, or .pi)
"""
return project_root / self.config_dir_name
def get_agent_path(self, agent: str, project_root: Path) -> Path:
"""Get path to agent definition file.
Args:
agent: Agent name (original, before mapping)
project_root: Project root directory
Returns:
Path to agent definition file (.md for most platforms, .toml for Codex)
"""
mapped_name = self.get_agent_name(agent)
if self.platform == "codex":
return self.get_config_dir(project_root) / "agents" / f"{mapped_name}.toml"
return self.get_config_dir(project_root) / "agents" / f"{mapped_name}.md"
def get_commands_path(self, project_root: Path, *parts: str) -> Path:
"""Get path to commands directory or specific command file.
Args:
project_root: Project root directory
*parts: Additional path parts (e.g., 'trellis', 'finish-work.md')
Returns:
Path to commands directory or file
Note:
Cursor uses prefix naming: .cursor/commands/trellis-<name>.md
Antigravity uses workflow directory: .agent/workflows/<name>.md
Windsurf uses workflow directory: .windsurf/workflows/trellis-<name>.md
Copilot uses prompt files: .github/prompts/<name>.prompt.md
Pi uses prompt templates: .pi/prompts/trellis-<name>.md
Claude/OpenCode use subdirectory: .claude/commands/trellis/<name>.md
"""
if self.platform == "pi":
prompts_dir = self.get_config_dir(project_root) / "prompts"
if not parts:
return prompts_dir
if len(parts) >= 2 and parts[0] == "trellis":
filename = parts[-1]
if filename.endswith(".md"):
filename = filename[:-3]
return prompts_dir / f"trellis-{filename}.md"
return prompts_dir / Path(*parts)
if self.platform == "windsurf":
workflow_dir = self.get_config_dir(project_root) / "workflows"
if not parts:
return workflow_dir
if len(parts) >= 2 and parts[0] == "trellis":
filename = parts[-1]
return workflow_dir / f"trellis-{filename}"
return workflow_dir / Path(*parts)
if self.platform in ("antigravity", "kilo"):
workflow_dir = self.get_config_dir(project_root) / "workflows"
if not parts:
return workflow_dir
if len(parts) >= 2 and parts[0] == "trellis":
filename = parts[-1]
return workflow_dir / filename
return workflow_dir / Path(*parts)
if self.platform == "copilot":
prompts_dir = project_root / ".github" / "prompts"
if not parts:
return prompts_dir
if len(parts) >= 2 and parts[0] == "trellis":
filename = parts[-1]
if filename.endswith(".md"):
filename = filename[:-3]
return prompts_dir / f"{filename}.prompt.md"
return prompts_dir / Path(*parts)
if not parts:
return self.get_config_dir(project_root) / "commands"
# Cursor uses prefix naming instead of subdirectory
if self.platform == "cursor" and len(parts) >= 2 and parts[0] == "trellis":
# Convert trellis/<name>.md to trellis-<name>.md
filename = parts[-1]
return (
self.get_config_dir(project_root) / "commands" / f"trellis-{filename}"
)
return self.get_config_dir(project_root) / "commands" / Path(*parts)
def get_trellis_command_path(self, name: str) -> str:
"""Get relative path to a trellis command file.
Args:
name: Command name without extension (e.g., 'finish-work', 'check')
Returns:
Relative path string for use in JSONL entries
Note:
Cursor: .cursor/commands/trellis-<name>.md
Codex: .agents/skills/trellis-<name>/SKILL.md
Kiro: .kiro/skills/trellis-<name>/SKILL.md
Gemini: .gemini/commands/trellis/<name>.toml
Antigravity: .agent/workflows/<name>.md
Windsurf: .windsurf/workflows/trellis-<name>.md
Pi: .pi/prompts/trellis-<name>.md
Others: .{platform}/commands/trellis/<name>.md
"""
if self.platform == "cursor":
return f".cursor/commands/trellis-{name}.md"
elif self.platform == "codex":
# 0.5.0-beta.0 renamed all skill dirs to add the `trellis-` prefix
# (see that release's manifest for the 60+ rename entries).
return f".agents/skills/trellis-{name}/SKILL.md"
elif self.platform == "kiro":
return f".kiro/skills/trellis-{name}/SKILL.md"
elif self.platform == "gemini":
return f".gemini/commands/trellis/{name}.toml"
elif self.platform == "antigravity":
return f".agent/workflows/{name}.md"
elif self.platform == "windsurf":
return f".windsurf/workflows/trellis-{name}.md"
elif self.platform == "kilo":
return f".kilocode/workflows/{name}.md"
elif self.platform == "copilot":
return f".github/prompts/{name}.prompt.md"
elif self.platform == "droid":
return f".factory/commands/trellis/{name}.md"
elif self.platform == "pi":
return f".pi/prompts/trellis-{name}.md"
else:
return f"{self.config_dir_name}/commands/trellis/{name}.md"
# =========================================================================
# Environment Variables
# =========================================================================
def get_non_interactive_env(self) -> dict[str, str]:
"""Get environment variables for non-interactive mode.
Returns:
Dict of environment variables to set
"""
if self.platform == "opencode":
return {"OPENCODE_NON_INTERACTIVE": "1"}
elif self.platform == "iflow":
return {"IFLOW_NON_INTERACTIVE": "1"}
elif self.platform == "codex":
return {"CODEX_NON_INTERACTIVE": "1"}
elif self.platform == "kiro":
return {"KIRO_NON_INTERACTIVE": "1"}
elif self.platform == "gemini":
return {} # Gemini CLI doesn't have a non-interactive env var
elif self.platform == "antigravity":
return {}
elif self.platform == "windsurf":
return {}
elif self.platform == "qoder":
return {}
elif self.platform == "codebuddy":
return {}
elif self.platform == "copilot":
return {}
elif self.platform == "droid":
return {}
elif self.platform == "pi":
return {}
else:
return {"CLAUDE_NON_INTERACTIVE": "1"}
# =========================================================================
# CLI Command Building
# =========================================================================
def build_run_command(
self,
agent: str,
prompt: str,
session_id: str | None = None,
skip_permissions: bool = True,
verbose: bool = True,
json_output: bool = True,
) -> list[str]:
"""Build CLI command for running an agent.
Args:
agent: Agent name (will be mapped if needed)
prompt: Prompt to send to the agent
session_id: Optional session ID (Claude Code only for creation)
skip_permissions: Whether to skip permission prompts
verbose: Whether to enable verbose output
json_output: Whether to use JSON output format
Returns:
List of command arguments
"""
mapped_agent = self.get_agent_name(agent)
if self.platform == "opencode":
cmd = ["opencode", "run"]
cmd.extend(["--agent", mapped_agent])
# Note: OpenCode 'run' mode is non-interactive by default
# No equivalent to Claude Code's --dangerously-skip-permissions
# See: https://github.com/anomalyco/opencode/issues/9070
if json_output:
cmd.extend(["--format", "json"])
if verbose:
cmd.extend(["--log-level", "DEBUG", "--print-logs"])
# Note: OpenCode doesn't support --session-id on creation
# Session ID must be extracted from logs after startup
cmd.append(prompt)
elif self.platform == "iflow":
cmd = ["iflow", "-y", "-p"]
cmd.append(f"${mapped_agent} {prompt}")
elif self.platform == "codex":
cmd = ["codex", "exec"]
cmd.append(prompt)
elif self.platform == "kiro":
cmd = ["kiro", "run", prompt]
elif self.platform == "gemini":
cmd = ["gemini"]
cmd.append(prompt)
elif self.platform == "antigravity":
raise ValueError(
"Antigravity workflows are UI slash commands; CLI agent run is not supported."
)
elif self.platform == "windsurf":
raise ValueError(
"Windsurf workflows are UI slash commands; CLI agent run is not supported."
)
elif self.platform == "qoder":
cmd = ["qodercli", "-p", prompt]
elif self.platform == "codebuddy":
raise ValueError(
"CodeBuddy does not support non-interactive mode (no CLI agent)"
)
elif self.platform == "copilot":
raise ValueError(
"GitHub Copilot is IDE-only; CLI agent run is not supported."
)
elif self.platform == "droid":
raise ValueError(
"Factory Droid CLI agent run is not yet supported."
)
elif self.platform == "pi":
cmd = ["pi", "-p", prompt]
else: # claude
cmd = ["claude", "-p"]
cmd.extend(["--agent", mapped_agent])
if session_id:
cmd.extend(["--session-id", session_id])
if skip_permissions:
cmd.append("--dangerously-skip-permissions")
if json_output:
cmd.extend(["--output-format", "stream-json"])
if verbose:
cmd.append("--verbose")
cmd.append(prompt)
return cmd
def build_resume_command(self, session_id: str) -> list[str]:
"""Build CLI command for resuming a session.
Args:
session_id: Session ID to resume (ignored for iFlow)
Returns:
List of command arguments
"""
if self.platform == "opencode":
return ["opencode", "run", "--session", session_id]
elif self.platform == "iflow":
# iFlow uses -c to continue most recent conversation
# session_id is ignored as iFlow doesn't support session IDs
return ["iflow", "-c"]
elif self.platform == "codex":
return ["codex", "resume", session_id]
elif self.platform == "kiro":
return ["kiro", "resume", session_id]
elif self.platform == "gemini":
return ["gemini", "--resume", session_id]
elif self.platform == "antigravity":
raise ValueError(
"Antigravity workflows are UI slash commands; CLI resume is not supported."
)
elif self.platform == "windsurf":
raise ValueError(
"Windsurf workflows are UI slash commands; CLI resume is not supported."
)
elif self.platform == "qoder":
return ["qodercli", "--resume", session_id]
elif self.platform == "codebuddy":
raise ValueError(
"CodeBuddy does not support non-interactive mode (no CLI agent)"
)
elif self.platform == "copilot":
raise ValueError(
"GitHub Copilot is IDE-only; CLI resume is not supported."
)
elif self.platform == "droid":
raise ValueError(
"Factory Droid CLI resume is not yet supported."
)
elif self.platform == "pi":
return ["pi", "-c", session_id]
else:
return ["claude", "--resume", session_id]
def get_resume_command_str(self, session_id: str, cwd: str | None = None) -> str:
"""Get human-readable resume command string.
Args:
session_id: Session ID to resume
cwd: Optional working directory to cd into
Returns:
Command string for display
"""
cmd = self.build_resume_command(session_id)
cmd_str = " ".join(cmd)
if cwd:
return f"cd {cwd} && {cmd_str}"
return cmd_str
# =========================================================================
# Platform Detection Helpers
# =========================================================================
@property
def is_opencode(self) -> bool:
"""Check if platform is OpenCode."""
return self.platform == "opencode"
@property
def is_claude(self) -> bool:
"""Check if platform is Claude Code."""
return self.platform == "claude"
@property
def is_cursor(self) -> bool:
"""Check if platform is Cursor."""
return self.platform == "cursor"
@property
def is_iflow(self) -> bool:
"""Check if platform is iFlow CLI."""
return self.platform == "iflow"
@property
def cli_name(self) -> str:
"""Get CLI executable name.
Note: Cursor doesn't have a CLI tool, returns None-like value.
"""
if self.is_opencode:
return "opencode"
elif self.is_cursor:
return "cursor" # Note: Cursor is IDE-only, no CLI
elif self.platform == "iflow":
return "iflow"
elif self.platform == "kiro":
return "kiro"
elif self.platform == "gemini":
return "gemini"
elif self.platform == "antigravity":
return "agy"
elif self.platform == "windsurf":
return "windsurf"
elif self.platform == "qoder":
return "qodercli"
elif self.platform == "codebuddy":
return "codebuddy"
elif self.platform == "copilot":
return "copilot"
elif self.platform == "droid":
return "droid"
elif self.platform == "pi":
return "pi"
else:
return "claude"
@property
def supports_cli_agents(self) -> bool:
"""Check if platform supports running agents via CLI.
Claude Code, OpenCode, iFlow, and Codex support CLI agent execution.
Cursor is IDE-only and doesn't support CLI agents.
"""
return self.platform in ("claude", "opencode", "iflow", "codex", "pi")
@property
def requires_agent_definition_file(self) -> bool:
"""Check if platform requires an agent definition file (.md/.toml) to run.
Claude Code, OpenCode, iFlow: require agent .md files (--agent flag).
Codex: auto-discovers agents from .codex/agents/*.toml, no --agent flag.
"""
return self.platform in ("claude", "opencode", "iflow")
# =========================================================================
# Session ID Handling
# =========================================================================
@property
def supports_session_id_on_create(self) -> bool:
"""Check if platform supports specifying session ID on creation.
Claude Code: Yes (--session-id)
OpenCode: No (auto-generated, extract from logs)
iFlow: No (no session ID support)
"""
return self.platform == "claude"
def extract_session_id_from_log(self, log_content: str) -> str | None:
"""Extract session ID from log output (OpenCode only).
OpenCode generates session IDs in format: ses_xxx
Args:
log_content: Log file content
Returns:
Session ID if found, None otherwise
"""
import re
# OpenCode session ID pattern
match = re.search(r"ses_[a-zA-Z0-9]+", log_content)
if match:
return match.group(0)
return None
# =============================================================================
# Factory Function
# =============================================================================
def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
"""Get CLI adapter for the specified platform.
Args:
platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi')
Returns:
CLIAdapter instance
Raises:
ValueError: If platform is not supported
"""
if platform not in (
"claude",
"opencode",
"cursor",
"iflow",
"codex",
"kilo",
"kiro",
"gemini",
"antigravity",
"windsurf",
"qoder",
"codebuddy",
"copilot",
"droid",
"pi",
):
raise ValueError(
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi')"
)
return CLIAdapter(platform=platform) # type: ignore
_ALL_PLATFORM_CONFIG_DIRS = (
".claude",
".cursor",
".iflow",
".opencode",
".codex",
".kilocode",
".kiro",
".gemini",
".agent",
".windsurf",
".qoder",
".codebuddy",
".github/copilot",
".factory",
".pi",
)
"""Platform-specific config directory names used by detect_platform exclusion
checks. `.agents/skills/` is NOT listed here: it is a shared cross-platform
layer (written by Codex, also consumed by Amp/Cline/Warp/etc. via the
agentskills.io standard), not a single-platform signal. Its presence must not
block detection of Kiro, Antigravity, Windsurf, or other platforms."""
def _has_other_platform_dir(project_root: Path, exclude: set[str]) -> bool:
"""Check if any platform config dir exists besides those in *exclude*."""
return any(
(project_root / d).is_dir()
for d in _ALL_PLATFORM_CONFIG_DIRS
if d not in exclude
)
def detect_platform(project_root: Path) -> Platform:
"""Auto-detect platform based on existing config directories.
Detection order:
1. TRELLIS_PLATFORM environment variable (if set)
2. .opencode directory exists → opencode
3. .iflow directory exists → iflow
4. .cursor directory exists (without .claude) → cursor
5. .codex exists and no other platform dirs → codex
6. .kilocode directory exists → kilo
7. .kiro/skills exists and no other platform dirs → kiro
8. .gemini directory exists → gemini
9. .agent/workflows exists and no other platform dirs → antigravity
10. .windsurf/workflows exists and no other platform dirs → windsurf
11. .codebuddy directory exists → codebuddy
12. .qoder directory exists → qoder
13. .pi directory exists → pi
14. Default → claude
Args:
project_root: Project root directory
Returns:
Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or default 'claude')
"""
import os
# Check environment variable first
env_platform = os.environ.get("TRELLIS_PLATFORM", "").lower()
if env_platform in (
"claude",
"opencode",
"cursor",
"iflow",
"codex",
"kilo",
"kiro",
"gemini",
"antigravity",
"windsurf",
"qoder",
"codebuddy",
"copilot",
"droid",
"pi",
):
return env_platform # type: ignore
# Check for .opencode directory (OpenCode-specific)
if (project_root / ".opencode").is_dir():
return "opencode"
# Check for .iflow directory (iFlow-specific)
if (project_root / ".iflow").is_dir():
return "iflow"
# Check for .cursor directory (Cursor-specific)
# Only detect as cursor if .claude doesn't exist (to avoid confusion)
if (project_root / ".cursor").is_dir() and not (project_root / ".claude").is_dir():
return "cursor"
# Check for .gemini directory (Gemini CLI-specific)
if (project_root / ".gemini").is_dir():
return "gemini"
# Check for .codex directory (Codex-specific)
# .agents/skills/ alone does NOT trigger codex detection (it's a shared standard)
if (project_root / ".codex").is_dir() and not _has_other_platform_dir(
project_root, {".codex", ".agents"}
):
return "codex"
# Check for .kilocode directory (Kilo-specific)
if (project_root / ".kilocode").is_dir():
return "kilo"
# Check for Kiro skills directory only when no other platform config exists
if (project_root / ".kiro" / "skills").is_dir() and not _has_other_platform_dir(
project_root, {".kiro"}
):
return "kiro"
# Check for Antigravity workflow directory only when no other platform config exists
if (
project_root / ".agent" / "workflows"
).is_dir() and not _has_other_platform_dir(
project_root, {".agent", ".gemini"}
):
return "antigravity"
# Check for Windsurf workflow directory only when no other platform config exists
if (
project_root / ".windsurf" / "workflows"
).is_dir() and not _has_other_platform_dir(
project_root, {".windsurf"}
):
return "windsurf"
# Check for .codebuddy directory (CodeBuddy-specific)
if (project_root / ".codebuddy").is_dir():
return "codebuddy"
# Check for .qoder directory (Qoder-specific)
if (project_root / ".qoder").is_dir():
return "qoder"
# Check for .github/copilot directory (GitHub Copilot-specific)
if (project_root / ".github" / "copilot").is_dir():
return "copilot"
# Check for .factory directory (Factory Droid-specific)
if (project_root / ".factory").is_dir():
return "droid"
# Check for .pi directory (Pi Agent-specific)
if (project_root / ".pi").is_dir():
return "pi"
# Fallback: checkout only has the Codex shared-skills layer
# (.agents/skills/trellis-* dirs) and no explicit platform config dir.
# Happens on fresh clones where .codex/ is gitignored/absent but the
# shared skills were committed to git. Must guard against the case
# where .claude/ or any other platform dir also exists — .agents/skills/
# can legitimately coexist with any platform as a shared consumption
# layer for Amp/Cline/Warp/etc.
agents_skills = project_root / ".agents" / "skills"
if agents_skills.is_dir() and not _has_other_platform_dir(
project_root, set()
):
try:
for entry in agents_skills.iterdir():
if entry.is_dir() and entry.name.startswith("trellis-"):
return "codex"
except OSError:
pass
return "claude"
def get_cli_adapter_auto(project_root: Path) -> CLIAdapter:
"""Get CLI adapter with auto-detected platform.
Args:
project_root: Project root directory
Returns:
CLIAdapter instance for detected platform
"""
platform = detect_platform(project_root)
return CLIAdapter(platform=platform)

View File

@@ -0,0 +1,389 @@
#!/usr/bin/env python3
"""
Trellis configuration reader.
Reads settings from .trellis/config.yaml with sensible defaults.
"""
from __future__ import annotations
import sys
from pathlib import Path
from .paths import DIR_WORKFLOW, get_repo_root
# =============================================================================
# YAML Simple Parser (no dependencies)
# =============================================================================
def _unquote(s: str) -> str:
"""Remove exactly one layer of matching surrounding quotes.
Unlike str.strip('"'), this only removes the outermost pair,
preserving any nested quotes inside the value.
Examples:
_unquote('"hello"') -> 'hello'
_unquote("'hello'") -> 'hello'
_unquote('"echo \\'hi\\'"') -> "echo 'hi'"
_unquote('hello') -> 'hello'
_unquote('"hello\\'') -> '"hello\\'' (mismatched, unchanged)
"""
if len(s) >= 2 and s[0] == s[-1] and s[0] in ('"', "'"):
return s[1:-1]
return s
def parse_simple_yaml(content: str) -> dict:
"""Parse simple YAML with nested dict support (no dependencies).
Supports:
- key: value (string)
- key: (followed by list items)
- item1
- item2
- key: (followed by nested dict)
nested_key: value
nested_key2:
- item
Uses indentation to detect nesting (2+ spaces deeper = child).
Args:
content: YAML content string.
Returns:
Parsed dict (values can be str, list[str], or dict).
"""
lines = content.splitlines()
result: dict = {}
_parse_yaml_block(lines, 0, 0, result)
return result
def _parse_yaml_block(
lines: list[str], start: int, min_indent: int, target: dict
) -> int:
"""Parse a YAML block into target dict, returning next line index."""
i = start
current_list: list | None = None
while i < len(lines):
line = lines[i]
stripped = line.strip()
# Skip empty lines and comments
if not stripped or stripped.startswith("#"):
i += 1
continue
# Calculate indentation
indent = len(line) - len(line.lstrip())
# If dedented past our block, we're done
if indent < min_indent:
break
if stripped.startswith("- "):
if current_list is not None:
current_list.append(_unquote(stripped[2:].strip()))
i += 1
elif ":" in stripped:
key, _, value = stripped.partition(":")
key = key.strip()
value = _unquote(value.strip())
current_list = None
if value:
# key: value
target[key] = value
i += 1
else:
# key: (no value) — peek ahead to determine list vs nested dict
next_i, next_line = _next_content_line(lines, i + 1)
if next_i >= len(lines):
target[key] = {}
i = next_i
elif next_line.strip().startswith("- "):
# It's a list
current_list = []
target[key] = current_list
i += 1
else:
next_indent = len(next_line) - len(next_line.lstrip())
if next_indent > indent:
# It's a nested dict
nested: dict = {}
target[key] = nested
i = _parse_yaml_block(lines, i + 1, next_indent, nested)
else:
# Empty value, same or less indent follows
target[key] = {}
i += 1
else:
i += 1
return i
def _next_content_line(lines: list[str], start: int) -> tuple[int, str]:
"""Find the next non-empty, non-comment line."""
i = start
while i < len(lines):
stripped = lines[i].strip()
if stripped and not stripped.startswith("#"):
return i, lines[i]
i += 1
return i, ""
# Defaults
DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal"
DEFAULT_MAX_JOURNAL_LINES = 2000
CONFIG_FILE = "config.yaml"
def _is_true_config_value(value: object) -> bool:
"""Return True when a config value represents an enabled flag."""
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.strip().lower() == "true"
return False
def _get_config_path(repo_root: Path | None = None) -> Path:
"""Get path to config.yaml."""
root = repo_root or get_repo_root()
return root / DIR_WORKFLOW / CONFIG_FILE
def _load_config(repo_root: Path | None = None) -> dict:
"""Load and parse config.yaml. Returns empty dict on any error."""
config_file = _get_config_path(repo_root)
try:
content = config_file.read_text(encoding="utf-8")
return parse_simple_yaml(content)
except (OSError, IOError):
return {}
def get_session_commit_message(repo_root: Path | None = None) -> str:
"""Get the commit message for auto-committing session records."""
config = _load_config(repo_root)
return config.get("session_commit_message", DEFAULT_SESSION_COMMIT_MESSAGE)
def get_max_journal_lines(repo_root: Path | None = None) -> int:
"""Get the maximum lines per journal file."""
config = _load_config(repo_root)
value = config.get("max_journal_lines", DEFAULT_MAX_JOURNAL_LINES)
try:
return int(value)
except (ValueError, TypeError):
return DEFAULT_MAX_JOURNAL_LINES
def get_hooks(event: str, repo_root: Path | None = None) -> list[str]:
"""Get hook commands for a lifecycle event.
Args:
event: Event name (e.g. "after_create", "after_archive").
repo_root: Repository root path.
Returns:
List of shell commands to execute, empty if none configured.
"""
config = _load_config(repo_root)
hooks = config.get("hooks")
if not isinstance(hooks, dict):
return []
commands = hooks.get(event)
if isinstance(commands, list):
return [str(c) for c in commands]
return []
# =============================================================================
# Monorepo / Packages
# =============================================================================
def get_packages(repo_root: Path | None = None) -> dict[str, dict] | None:
"""Get monorepo package declarations.
Returns:
Dict mapping package name to its config (path, type, etc.),
or None if not configured (single-repo mode).
Example return:
{"cli": {"path": "packages/cli"}, "docs-site": {"path": "docs-site", "type": "submodule"}}
"""
config = _load_config(repo_root)
packages = config.get("packages")
if not isinstance(packages, dict):
return None
# Ensure each value is a dict (filter out scalar entries)
filtered = {k: v for k, v in packages.items() if isinstance(v, dict)}
if not filtered:
return None
return filtered
def get_default_package(repo_root: Path | None = None) -> str | None:
"""Get the default package name from config.
Returns:
Package name string, or None if not configured.
"""
config = _load_config(repo_root)
value = config.get("default_package")
return str(value) if value else None
def get_submodule_packages(repo_root: Path | None = None) -> dict[str, str]:
"""Get packages that are git submodules.
Returns:
Dict mapping package name to its path for submodule-type packages.
Empty dict if none configured.
Example return:
{"docs-site": "docs-site"}
"""
packages = get_packages(repo_root)
if packages is None:
return {}
return {
name: cfg.get("path", name)
for name, cfg in packages.items()
if cfg.get("type") == "submodule"
}
def get_git_packages(repo_root: Path | None = None) -> dict[str, str]:
"""Get packages that have their own independent git repository.
These are sub-directories with their own .git (not submodules),
marked with ``git: true`` in config.yaml.
Returns:
Dict mapping package name to its path for git-repo packages.
Empty dict if none configured.
Example config::
packages:
backend:
path: iqs
git: true
Example return::
{"backend": "iqs"}
"""
packages = get_packages(repo_root)
if packages is None:
return {}
return {
name: cfg.get("path", name)
for name, cfg in packages.items()
if _is_true_config_value(cfg.get("git"))
}
def is_monorepo(repo_root: Path | None = None) -> bool:
"""Check if the project is configured as a monorepo (has packages in config)."""
return get_packages(repo_root) is not None
def get_spec_base(package: str | None = None, repo_root: Path | None = None) -> str:
"""Get the spec directory base path relative to .trellis/.
Single-repo: returns "spec"
Monorepo with package: returns "spec/<package>"
Monorepo without package: returns "spec" (caller should specify package)
"""
if package and is_monorepo(repo_root):
return f"spec/{package}"
return "spec"
def validate_package(package: str, repo_root: Path | None = None) -> bool:
"""Check if a package name is valid in this project.
Single-repo (no packages configured): always returns True.
Monorepo: returns True only if package exists in config.yaml packages.
"""
packages = get_packages(repo_root)
if packages is None:
return True # Single-repo, no validation needed
return package in packages
def resolve_package(
task_package: str | None = None,
repo_root: Path | None = None,
) -> str | None:
"""Resolve package from inferred sources with validation.
Checks in order: task_package → default_package.
Invalid inferred values print a warning to stderr and are skipped.
Returns:
Resolved package name, or None if no valid package found.
Note:
CLI --package should be validated separately by the caller
(fail-fast with available packages list on error).
"""
packages = get_packages(repo_root)
if packages is None:
return None # Single-repo, no package needed
# Try task_package (guard against non-string values from malformed JSON)
if task_package and isinstance(task_package, str):
if task_package in packages:
return task_package
print(
f"Warning: task.json package '{task_package}' not found in config, skipping",
file=sys.stderr,
)
# Try default_package
default = get_default_package(repo_root)
if default:
if default in packages:
return default
print(
f"Warning: default_package '{default}' not found in config, skipping",
file=sys.stderr,
)
return None
def get_spec_scope(repo_root: Path | None = None) -> list[str] | str | None:
"""Get session.spec_scope configuration.
Returns:
list[str]: Package names to include in spec scanning.
str: "active_task" to use current task's package.
None: No scope configured (scan all packages).
"""
config = _load_config(repo_root)
session = config.get("session")
if not isinstance(session, dict):
return None
scope = session.get("spec_scope")
if scope is None:
return None
if isinstance(scope, str):
return scope # e.g. "active_task"
if isinstance(scope, list):
return [str(s) for s in scope]
return None

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
Developer management utilities.
Provides:
init_developer - Initialize developer
ensure_developer - Ensure developer is initialized (exit if not)
show_developer_info - Show developer information
"""
from __future__ import annotations
import sys
from datetime import datetime
from pathlib import Path
from .paths import (
DIR_WORKFLOW,
DIR_WORKSPACE,
DIR_TASKS,
FILE_DEVELOPER,
FILE_JOURNAL_PREFIX,
get_repo_root,
get_developer,
check_developer,
)
# =============================================================================
# Developer Initialization
# =============================================================================
def init_developer(name: str, repo_root: Path | None = None) -> bool:
"""Initialize developer.
Creates:
- .trellis/.developer file with developer info
- .trellis/workspace/<name>/ directory structure
- Initial journal file and index.md
Args:
name: Developer name.
repo_root: Repository root path. Defaults to auto-detected.
Returns:
True on success, False on error.
"""
if not name:
print("Error: developer name is required", file=sys.stderr)
return False
if repo_root is None:
repo_root = get_repo_root()
dev_file = repo_root / DIR_WORKFLOW / FILE_DEVELOPER
workspace_dir = repo_root / DIR_WORKFLOW / DIR_WORKSPACE / name
# Create .developer file
initialized_at = datetime.now().isoformat()
try:
dev_file.write_text(
f"name={name}\ninitialized_at={initialized_at}\n",
encoding="utf-8"
)
except (OSError, IOError) as e:
print(f"Error: Failed to create .developer file: {e}", file=sys.stderr)
return False
# Create workspace directory structure
try:
workspace_dir.mkdir(parents=True, exist_ok=True)
except (OSError, IOError) as e:
print(f"Error: Failed to create workspace directory: {e}", file=sys.stderr)
return False
# Create initial journal file
journal_file = workspace_dir / f"{FILE_JOURNAL_PREFIX}1.md"
if not journal_file.exists():
today = datetime.now().strftime("%Y-%m-%d")
journal_content = f"""# Journal - {name} (Part 1)
> AI development session journal
> Started: {today}
---
"""
try:
journal_file.write_text(journal_content, encoding="utf-8")
except (OSError, IOError) as e:
print(f"Error: Failed to create journal file: {e}", file=sys.stderr)
return False
# Create index.md with markers for auto-update
index_file = workspace_dir / "index.md"
if not index_file.exists():
index_content = f"""# Workspace Index - {name}
> Journal tracking for AI development sessions.
---
## Current Status
<!-- @@@auto:current-status -->
- **Active File**: `journal-1.md`
- **Total Sessions**: 0
- **Last Active**: -
<!-- @@@/auto:current-status -->
---
## Active Documents
<!-- @@@auto:active-documents -->
| File | Lines | Status |
|------|-------|--------|
| `journal-1.md` | ~0 | Active |
<!-- @@@/auto:active-documents -->
---
## Session History
<!-- @@@auto:session-history -->
| # | Date | Title | Commits | Branch |
|---|------|-------|---------|--------|
<!-- @@@/auto:session-history -->
---
## Notes
- Sessions are appended to journal files
- New journal file created when current exceeds 2000 lines
- Use `add_session.py` to record sessions
"""
try:
index_file.write_text(index_content, encoding="utf-8")
except (OSError, IOError) as e:
print(f"Error: Failed to create index.md: {e}", file=sys.stderr)
return False
print(f"Developer initialized: {name}")
print(f" .developer file: {dev_file}")
print(f" Workspace dir: {workspace_dir}")
return True
def ensure_developer(repo_root: Path | None = None) -> None:
"""Ensure developer is initialized, exit if not.
Args:
repo_root: Repository root path. Defaults to auto-detected.
"""
if repo_root is None:
repo_root = get_repo_root()
if not check_developer(repo_root):
print("Error: Developer not initialized.", file=sys.stderr)
print(f"Run: python ./{DIR_WORKFLOW}/scripts/init_developer.py <your-name>", file=sys.stderr)
sys.exit(1)
def show_developer_info(repo_root: Path | None = None) -> None:
"""Show developer information.
Args:
repo_root: Repository root path. Defaults to auto-detected.
"""
if repo_root is None:
repo_root = get_repo_root()
developer = get_developer(repo_root)
if not developer:
print("Developer: (not initialized)")
else:
print(f"Developer: {developer}")
print(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/")
print(f"Tasks: {DIR_WORKFLOW}/{DIR_TASKS}/")
# =============================================================================
# Main Entry (for testing)
# =============================================================================
if __name__ == "__main__":
show_developer_info()

View File

@@ -0,0 +1,31 @@
"""
Git command execution utility.
Single source of truth for running git commands across all Trellis scripts.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
def run_git(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
"""Run a git command and return (returncode, stdout, stderr).
Uses UTF-8 encoding with -c i18n.logOutputEncoding=UTF-8 to ensure
consistent output across all platforms (Windows, macOS, Linux).
"""
try:
git_args = ["git", "-c", "i18n.logOutputEncoding=UTF-8"] + args
result = subprocess.run(
git_args,
cwd=cwd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
return result.returncode, result.stdout, result.stderr
except Exception as e:
return 1, "", str(e)

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Git and Session Context utilities.
Entry shim — delegates to session_context and packages_context.
Provides:
output_json - Output context in JSON format
output_text - Output context in text format
"""
from __future__ import annotations
import json
from .git import run_git
from .session_context import (
get_context_json,
get_context_text,
get_context_record_json,
get_context_text_record,
output_json,
output_text,
)
from .packages_context import (
get_context_packages_text,
get_context_packages_json,
)
from .workflow_phase import (
filter_platform,
get_phase_index,
get_step,
)
# Backward-compatible alias — external modules import this name
_run_git_command = run_git
# =============================================================================
# Main Entry
# =============================================================================
def main() -> None:
"""CLI entry point."""
import argparse
parser = argparse.ArgumentParser(description="Get Session Context for AI Agent")
parser.add_argument(
"--json",
"-j",
action="store_true",
help="Output in JSON format (works with any --mode)",
)
parser.add_argument(
"--mode",
"-m",
choices=["default", "record", "packages", "phase"],
default="default",
help="Output mode: default (full context), record (for record-session), packages (package info only), phase (workflow step extraction)",
)
parser.add_argument(
"--step",
help="Step id for --mode phase, e.g. 1.1, 2.2. Omit to get the Phase Index.",
)
parser.add_argument(
"--platform",
help="Platform name for --mode phase, e.g. cursor, claude-code. Filters platform-tagged blocks.",
)
args = parser.parse_args()
if args.mode == "record":
if args.json:
print(json.dumps(get_context_record_json(), indent=2, ensure_ascii=False))
else:
print(get_context_text_record())
elif args.mode == "packages":
if args.json:
print(json.dumps(get_context_packages_json(), indent=2, ensure_ascii=False))
else:
print(get_context_packages_text())
elif args.mode == "phase":
content = get_step(args.step) if args.step else get_phase_index()
if not content.strip():
if args.step:
parser.exit(2, f"Step not found: {args.step}\n")
else:
parser.exit(2, "Phase Index section not found in workflow.md\n")
if args.platform:
content = filter_platform(content, args.platform)
print(content, end="")
else:
if args.json:
output_json()
else:
output_text()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,37 @@
"""
JSON file I/O utilities.
Provides read_json and write_json as the single source of truth
for JSON file operations across all Trellis scripts.
"""
from __future__ import annotations
import json
from pathlib import Path
def read_json(path: Path) -> dict | None:
"""Read and parse a JSON file.
Returns None if the file doesn't exist, is invalid JSON, or can't be read.
"""
try:
return json.loads(path.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError, OSError):
return None
def write_json(path: Path, data: dict) -> bool:
"""Write dict to JSON file with pretty formatting.
Returns True on success, False on error.
"""
try:
path.write_text(
json.dumps(data, indent=2, ensure_ascii=False),
encoding="utf-8",
)
return True
except (OSError, IOError):
return False

View File

@@ -0,0 +1,45 @@
"""
Terminal output utilities: colors and structured logging.
Single source of truth for Colors and log_* functions
used across all Trellis scripts.
"""
from __future__ import annotations
class Colors:
"""ANSI color codes for terminal output."""
RED = "\033[0;31m"
GREEN = "\033[0;32m"
YELLOW = "\033[1;33m"
BLUE = "\033[0;34m"
CYAN = "\033[0;36m"
DIM = "\033[2m"
NC = "\033[0m" # No Color / Reset
def colored(text: str, color: str) -> str:
"""Apply ANSI color to text."""
return f"{color}{text}{Colors.NC}"
def log_info(msg: str) -> None:
"""Print info-level message with [INFO] prefix."""
print(f"{Colors.BLUE}[INFO]{Colors.NC} {msg}")
def log_success(msg: str) -> None:
"""Print success message with [SUCCESS] prefix."""
print(f"{Colors.GREEN}[SUCCESS]{Colors.NC} {msg}")
def log_warn(msg: str) -> None:
"""Print warning message with [WARN] prefix."""
print(f"{Colors.YELLOW}[WARN]{Colors.NC} {msg}")
def log_error(msg: str) -> None:
"""Print error message with [ERROR] prefix."""
print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}")

View File

@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""
Package discovery and context output.
Provides:
get_packages_info - Get structured package info
get_packages_section - Build PACKAGES text section
get_context_packages_text - Full packages text output (--mode packages)
get_context_packages_json - Full packages JSON output (--mode packages --json)
"""
from __future__ import annotations
from pathlib import Path
from .config import _is_true_config_value, get_default_package, get_packages, get_spec_scope
from .paths import (
DIR_SPEC,
DIR_WORKFLOW,
get_current_task,
get_repo_root,
)
from .tasks import load_task
# =============================================================================
# Internal Helpers
# =============================================================================
def _scan_spec_layers(spec_dir: Path, package: str | None = None) -> list[str]:
"""Scan spec directory for available layers (subdirectories).
For monorepo: scans spec/<package>/
For single-repo: scans spec/
"""
target = spec_dir / package if package else spec_dir
if not target.is_dir():
return []
return sorted(
d.name for d in target.iterdir() if d.is_dir() and d.name != "guides"
)
def _get_active_task_package(repo_root: Path) -> str | None:
"""Get the package field from the active task's task.json."""
current = get_current_task(repo_root)
if not current:
return None
ct = load_task(repo_root / current)
return ct.package if ct and ct.package else None
def _resolve_scope_set(
packages: dict,
spec_scope,
task_pkg: str | None,
default_pkg: str | None,
) -> set | None:
"""Resolve spec_scope to a set of allowed package names, or None for full scan."""
if not packages:
return None
if spec_scope is None:
return None
if isinstance(spec_scope, str) and spec_scope == "active_task":
if task_pkg and task_pkg in packages:
return {task_pkg}
if default_pkg and default_pkg in packages:
return {default_pkg}
return None
if isinstance(spec_scope, list):
valid = {e for e in spec_scope if e in packages}
if valid:
return valid
# All invalid: fallback
if task_pkg and task_pkg in packages:
return {task_pkg}
if default_pkg and default_pkg in packages:
return {default_pkg}
return None
return None
# =============================================================================
# Public Functions
# =============================================================================
def get_packages_info(repo_root: Path) -> list[dict]:
"""Get structured package info for monorepo projects.
Returns list of dicts with keys: name, path, type, default, specLayers,
isSubmodule, isGitRepo.
Returns empty list for single-repo projects.
"""
packages = get_packages(repo_root)
if not packages:
return []
default_pkg = get_default_package(repo_root)
spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
result = []
for pkg_name, pkg_config in packages.items():
pkg_path = pkg_config.get("path", pkg_name) if isinstance(pkg_config, dict) else str(pkg_config)
pkg_type = pkg_config.get("type", "local") if isinstance(pkg_config, dict) else "local"
pkg_git = pkg_config.get("git", False) if isinstance(pkg_config, dict) else False
layers = _scan_spec_layers(spec_dir, pkg_name)
result.append({
"name": pkg_name,
"path": pkg_path,
"type": pkg_type,
"default": pkg_name == default_pkg,
"specLayers": layers,
"isSubmodule": pkg_type == "submodule",
"isGitRepo": _is_true_config_value(pkg_git),
})
return result
def get_packages_section(repo_root: Path) -> str:
"""Build the PACKAGES section for text output."""
spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
pkg_info = get_packages_info(repo_root)
lines: list[str] = []
lines.append("## PACKAGES")
if not pkg_info:
lines.append("(single-repo mode)")
layers = _scan_spec_layers(spec_dir)
if layers:
lines.append(f"Spec layers: {', '.join(layers)}")
return "\n".join(lines)
default_pkg = get_default_package(repo_root)
for pkg in pkg_info:
layers_str = f" [{', '.join(pkg['specLayers'])}]" if pkg["specLayers"] else ""
submodule_tag = " (submodule)" if pkg["isSubmodule"] else ""
git_repo_tag = " (git repo)" if pkg["isGitRepo"] else ""
default_tag = " *" if pkg["default"] else ""
lines.append(
f"- {pkg['name']:<16} {pkg['path']:<20}{layers_str}{submodule_tag}{git_repo_tag}{default_tag}"
)
if default_pkg:
lines.append(f"Default package: {default_pkg}")
return "\n".join(lines)
def get_context_packages_text(repo_root: Path | None = None) -> str:
"""Get packages context as formatted text (for --mode packages)."""
if repo_root is None:
repo_root = get_repo_root()
pkg_info = get_packages_info(repo_root)
lines: list[str] = []
if not pkg_info:
spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
lines.append("Single-repo project (no packages configured)")
lines.append("")
layers = _scan_spec_layers(spec_dir)
if layers:
lines.append(f"Spec layers: {', '.join(layers)}")
return "\n".join(lines)
# Resolve scope for annotations
packages_dict = get_packages(repo_root) or {}
default_pkg = get_default_package(repo_root)
spec_scope = get_spec_scope(repo_root)
task_pkg = _get_active_task_package(repo_root)
scope_set = _resolve_scope_set(packages_dict, spec_scope, task_pkg, default_pkg)
lines.append("## PACKAGES")
lines.append("")
for pkg in pkg_info:
default_tag = " (default)" if pkg["default"] else ""
type_tag = f" [{pkg['type']}]" if pkg["type"] != "local" else ""
git_tag = " [git repo]" if pkg["isGitRepo"] else ""
# Scope annotation
scope_tag = ""
if scope_set is not None and pkg["name"] not in scope_set:
scope_tag = " (out of scope)"
lines.append(f"### {pkg['name']}{default_tag}{type_tag}{git_tag}{scope_tag}")
lines.append(f"Path: {pkg['path']}")
if pkg["specLayers"]:
lines.append(f"Spec layers: {', '.join(pkg['specLayers'])}")
for layer in pkg["specLayers"]:
lines.append(f" - .trellis/spec/{pkg['name']}/{layer}/index.md")
else:
lines.append("Spec: not configured")
lines.append("")
# Also show shared guides
guides_dir = repo_root / DIR_WORKFLOW / DIR_SPEC / "guides"
if guides_dir.is_dir():
lines.append("### Shared Guides (always included)")
lines.append("Path: .trellis/spec/guides/index.md")
lines.append("")
return "\n".join(lines)
def get_context_packages_json(repo_root: Path | None = None) -> dict:
"""Get packages context as a dictionary (for --mode packages --json)."""
if repo_root is None:
repo_root = get_repo_root()
pkg_info = get_packages_info(repo_root)
if not pkg_info:
spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
layers = _scan_spec_layers(spec_dir)
return {
"mode": "single-repo",
"specLayers": layers,
}
default_pkg = get_default_package(repo_root)
spec_scope = get_spec_scope(repo_root)
task_pkg = _get_active_task_package(repo_root)
return {
"mode": "monorepo",
"packages": pkg_info,
"defaultPackage": default_pkg,
"specScope": spec_scope,
"activeTaskPackage": task_pkg,
}

View File

@@ -0,0 +1,447 @@
#!/usr/bin/env python3
"""
Common path utilities for Trellis workflow.
Provides:
get_repo_root - Get repository root directory
get_developer - Get developer name
get_workspace_dir - Get developer workspace directory
get_tasks_dir - Get tasks directory
get_active_journal_file - Get current journal file
"""
from __future__ import annotations
import re
from datetime import datetime
from pathlib import Path
# =============================================================================
# Path Constants (change here to rename directories)
# =============================================================================
# Directory names
DIR_WORKFLOW = ".trellis"
DIR_WORKSPACE = "workspace"
DIR_TASKS = "tasks"
DIR_ARCHIVE = "archive"
DIR_SPEC = "spec"
DIR_SCRIPTS = "scripts"
# File names
FILE_DEVELOPER = ".developer"
FILE_CURRENT_TASK = ".current-task"
FILE_TASK_JSON = "task.json"
FILE_JOURNAL_PREFIX = "journal-"
# =============================================================================
# Repository Root
# =============================================================================
def get_repo_root(start_path: Path | None = None) -> Path:
"""Find the nearest directory containing .trellis/ folder.
This handles nested git repos correctly (e.g., test project inside another repo).
Args:
start_path: Starting directory to search from. Defaults to current directory.
Returns:
Path to repository root, or current directory if no .trellis/ found.
"""
current = (start_path or Path.cwd()).resolve()
while current != current.parent:
if (current / DIR_WORKFLOW).is_dir():
return current
current = current.parent
# Fallback to current directory if no .trellis/ found
return Path.cwd().resolve()
# =============================================================================
# Developer
# =============================================================================
def get_developer(repo_root: Path | None = None) -> str | None:
"""Get developer name from .developer file.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Developer name or None if not initialized.
"""
if repo_root is None:
repo_root = get_repo_root()
dev_file = repo_root / DIR_WORKFLOW / FILE_DEVELOPER
if not dev_file.is_file():
return None
try:
content = dev_file.read_text(encoding="utf-8")
for line in content.splitlines():
if line.startswith("name="):
return line.split("=", 1)[1].strip()
except (OSError, IOError):
pass
return None
def check_developer(repo_root: Path | None = None) -> bool:
"""Check if developer is initialized.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
True if developer is initialized.
"""
return get_developer(repo_root) is not None
# =============================================================================
# Tasks Directory
# =============================================================================
def get_tasks_dir(repo_root: Path | None = None) -> Path:
"""Get tasks directory path.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Path to tasks directory.
"""
if repo_root is None:
repo_root = get_repo_root()
return repo_root / DIR_WORKFLOW / DIR_TASKS
# =============================================================================
# Workspace Directory
# =============================================================================
def get_workspace_dir(repo_root: Path | None = None) -> Path | None:
"""Get developer workspace directory.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Path to workspace directory or None if developer not set.
"""
if repo_root is None:
repo_root = get_repo_root()
developer = get_developer(repo_root)
if developer:
return repo_root / DIR_WORKFLOW / DIR_WORKSPACE / developer
return None
# =============================================================================
# Journal File
# =============================================================================
def get_active_journal_file(repo_root: Path | None = None) -> Path | None:
"""Get the current active journal file.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Path to active journal file or None if not found.
"""
if repo_root is None:
repo_root = get_repo_root()
workspace_dir = get_workspace_dir(repo_root)
if workspace_dir is None or not workspace_dir.is_dir():
return None
latest: Path | None = None
highest = 0
for f in workspace_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
if not f.is_file():
continue
# Extract number from filename
name = f.stem # e.g., "journal-1"
match = re.search(r"(\d+)$", name)
if match:
num = int(match.group(1))
if num > highest:
highest = num
latest = f
return latest
def count_lines(file_path: Path) -> int:
"""Count lines in a file.
Args:
file_path: Path to file.
Returns:
Number of lines, or 0 if file doesn't exist.
"""
if not file_path.is_file():
return 0
try:
return len(file_path.read_text(encoding="utf-8").splitlines())
except (OSError, IOError):
return 0
# =============================================================================
# Current Task Management
# =============================================================================
def normalize_task_ref(task_ref: str) -> str:
"""Normalize a task ref for stable runtime storage.
Stored refs should prefer repo-relative POSIX paths like
`.trellis/tasks/03-27-my-task`, even on Windows. Absolute paths are preserved
unless they can later be converted back to repo-relative form by callers.
"""
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(f"{DIR_TASKS}/"):
return f"{DIR_WORKFLOW}/{normalized}"
return normalized
def resolve_task_ref(task_ref: str, repo_root: Path | None = None) -> Path | None:
"""Resolve a task ref to an absolute task directory path."""
if repo_root is None:
repo_root = get_repo_root()
normalized = normalize_task_ref(task_ref)
if not normalized:
return None
path_obj = Path(normalized)
if path_obj.is_absolute():
return path_obj
if normalized.startswith(f"{DIR_WORKFLOW}/"):
return repo_root / path_obj
return repo_root / DIR_WORKFLOW / DIR_TASKS / path_obj
def get_current_task(
repo_root: Path | None = None,
platform_input: dict | None = None,
platform: str | None = None,
) -> str | None:
"""Get current task directory path (relative to repo_root).
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Relative path to current task directory or None.
"""
if repo_root is None:
repo_root = get_repo_root()
from .active_task import resolve_active_task
return resolve_active_task(repo_root, platform_input, platform).task_path
def get_current_task_abs(
repo_root: Path | None = None,
platform_input: dict | None = None,
platform: str | None = None,
) -> Path | None:
"""Get current task directory absolute path.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Absolute path to current task directory or None.
"""
if repo_root is None:
repo_root = get_repo_root()
relative = get_current_task(repo_root, platform_input, platform)
if relative:
return resolve_task_ref(relative, repo_root)
return None
def get_current_task_source(
repo_root: Path | None = None,
platform_input: dict | None = None,
platform: str | None = None,
) -> tuple[str, str | None, str | None]:
"""Get active task source as (`source`, `context_key`, `task_path`)."""
if repo_root is None:
repo_root = get_repo_root()
from .active_task import get_current_task_source as _get_source
return _get_source(repo_root, platform_input, platform)
def set_current_task(
task_path: str,
repo_root: Path | None = None,
platform_input: dict | None = None,
platform: str | None = None,
) -> bool:
"""Set current task in session scope.
Args:
task_path: Task directory path (relative to repo_root).
repo_root: Repository root path. Defaults to auto-detected.
Returns:
True on success, False on error.
"""
if repo_root is None:
repo_root = get_repo_root()
from .active_task import set_active_task
return set_active_task(
task_path,
repo_root,
platform_input=platform_input,
platform=platform,
) is not None
def clear_current_task(
repo_root: Path | None = None,
platform_input: dict | None = None,
platform: str | None = None,
) -> bool:
"""Clear current task in session scope.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
True on success.
"""
if repo_root is None:
repo_root = get_repo_root()
from .active_task import clear_active_task
clear_active_task(
repo_root,
platform_input=platform_input,
platform=platform,
)
return True
def has_current_task(repo_root: Path | None = None) -> bool:
"""Check if has current task.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
True if current task is set.
"""
return get_current_task(repo_root) is not None
# =============================================================================
# Task ID Generation
# =============================================================================
def generate_task_date_prefix() -> str:
"""Generate task ID based on date (MM-DD format).
Returns:
Date prefix string (e.g., "01-21").
"""
return datetime.now().strftime("%m-%d")
# =============================================================================
# Monorepo / Package Paths
# =============================================================================
def get_spec_dir(package: str | None = None, repo_root: Path | None = None) -> Path:
"""Get the spec directory path.
Single-repo: .trellis/spec
Monorepo with package: .trellis/spec/<package>
Uses lazy import to avoid circular dependency with config.py.
"""
if repo_root is None:
repo_root = get_repo_root()
from .config import get_spec_base
base = get_spec_base(package, repo_root)
return repo_root / DIR_WORKFLOW / base
def get_package_path(package: str, repo_root: Path | None = None) -> Path | None:
"""Get a package's source directory absolute path from config.
Returns:
Absolute path to the package directory, or None if not found.
"""
if repo_root is None:
repo_root = get_repo_root()
from .config import get_packages
packages = get_packages(repo_root)
if not packages or package not in packages:
return None
info = packages[package]
if isinstance(info, dict):
rel_path = info.get("path", package)
else:
rel_path = str(info)
return repo_root / rel_path
# =============================================================================
# Main Entry (for testing)
# =============================================================================
if __name__ == "__main__":
repo = get_repo_root()
print(f"Repository root: {repo}")
print(f"Developer: {get_developer(repo)}")
print(f"Tasks dir: {get_tasks_dir(repo)}")
print(f"Workspace dir: {get_workspace_dir(repo)}")
print(f"Journal file: {get_active_journal_file(repo)}")
print(f"Current task: {get_current_task(repo)}")

View File

@@ -0,0 +1,574 @@
#!/usr/bin/env python3
"""
Session context generation (default + record modes).
Provides:
get_context_json - JSON output for default mode
get_context_text - Text output for default mode
get_context_record_json - JSON for record mode
get_context_text_record - Text for record mode
output_json - Print JSON
output_text - Print text
"""
from __future__ import annotations
import json
from pathlib import Path
from .config import get_git_packages
from .git import run_git
from .packages_context import get_packages_section
from .tasks import iter_active_tasks, load_task, get_all_statuses, children_progress
from .paths import (
DIR_SCRIPTS,
DIR_SPEC,
DIR_TASKS,
DIR_WORKFLOW,
DIR_WORKSPACE,
count_lines,
get_active_journal_file,
get_current_task,
get_current_task_source,
get_developer,
get_repo_root,
get_tasks_dir,
)
# =============================================================================
# Helpers
# =============================================================================
def _collect_package_git_info(repo_root: Path) -> list[dict]:
"""Collect git status and recent commits for packages with independent git repos.
Only packages marked with ``git: true`` in config.yaml are included.
Returns:
List of dicts with keys: name, path, branch, isClean,
uncommittedChanges, recentCommits.
Empty list if no git-repo packages are configured.
"""
git_pkgs = get_git_packages(repo_root)
if not git_pkgs:
return []
result = []
for pkg_name, pkg_path in git_pkgs.items():
pkg_dir = repo_root / pkg_path
if not (pkg_dir / ".git").exists():
continue
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=pkg_dir)
branch = branch_out.strip() or "unknown"
_, status_out, _ = run_git(["status", "--porcelain"], cwd=pkg_dir)
changes = len([l for l in status_out.splitlines() if l.strip()])
_, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=pkg_dir)
commits = []
for line in log_out.splitlines():
if line.strip():
parts = line.split(" ", 1)
if len(parts) >= 2:
commits.append({"hash": parts[0], "message": parts[1]})
elif len(parts) == 1:
commits.append({"hash": parts[0], "message": ""})
result.append({
"name": pkg_name,
"path": pkg_path,
"branch": branch,
"isClean": changes == 0,
"uncommittedChanges": changes,
"recentCommits": commits,
})
return result
def _append_package_git_context(lines: list[str], package_git_info: list[dict]) -> None:
"""Append Git status and recent commits for package repositories."""
for pkg in package_git_info:
lines.append(f"## GIT STATUS ({pkg['name']}: {pkg['path']})")
lines.append(f"Branch: {pkg['branch']}")
if pkg["isClean"]:
lines.append("Working directory: Clean")
else:
lines.append(
f"Working directory: {pkg['uncommittedChanges']} uncommitted change(s)"
)
lines.append("")
lines.append(f"## RECENT COMMITS ({pkg['name']}: {pkg['path']})")
if pkg["recentCommits"]:
for commit in pkg["recentCommits"]:
lines.append(f"{commit['hash']} {commit['message']}")
else:
lines.append("(no commits)")
lines.append("")
# =============================================================================
# JSON Output
# =============================================================================
def get_context_json(repo_root: Path | None = None) -> dict:
"""Get context as a dictionary.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Context dictionary.
"""
if repo_root is None:
repo_root = get_repo_root()
developer = get_developer(repo_root)
tasks_dir = get_tasks_dir(repo_root)
journal_file = get_active_journal_file(repo_root)
journal_lines = 0
journal_relative = ""
if journal_file and developer:
journal_lines = count_lines(journal_file)
journal_relative = (
f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
)
# Git info
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
branch = branch_out.strip() or "unknown"
_, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root)
git_status_count = len([line for line in status_out.splitlines() if line.strip()])
is_clean = git_status_count == 0
# Recent commits
_, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root)
commits = []
for line in log_out.splitlines():
if line.strip():
parts = line.split(" ", 1)
if len(parts) >= 2:
commits.append({"hash": parts[0], "message": parts[1]})
elif len(parts) == 1:
commits.append({"hash": parts[0], "message": ""})
# Tasks
tasks = [
{
"dir": t.dir_name,
"name": t.name,
"status": t.status,
"children": list(t.children),
"parent": t.parent,
}
for t in iter_active_tasks(tasks_dir)
]
# Package git repos (independent sub-repositories)
pkg_git_info = _collect_package_git_info(repo_root)
result = {
"developer": developer or "",
"git": {
"branch": branch,
"isClean": is_clean,
"uncommittedChanges": git_status_count,
"recentCommits": commits,
},
"tasks": {
"active": tasks,
"directory": f"{DIR_WORKFLOW}/{DIR_TASKS}",
},
"journal": {
"file": journal_relative,
"lines": journal_lines,
"nearLimit": journal_lines > 1800,
},
}
if pkg_git_info:
result["packageGit"] = pkg_git_info
return result
def output_json(repo_root: Path | None = None) -> None:
"""Output context in JSON format.
Args:
repo_root: Repository root path. Defaults to auto-detected.
"""
context = get_context_json(repo_root)
print(json.dumps(context, indent=2, ensure_ascii=False))
# =============================================================================
# Text Output
# =============================================================================
def get_context_text(repo_root: Path | None = None) -> str:
"""Get context as formatted text.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Formatted text output.
"""
if repo_root is None:
repo_root = get_repo_root()
lines = []
lines.append("========================================")
lines.append("SESSION CONTEXT")
lines.append("========================================")
lines.append("")
developer = get_developer(repo_root)
# Developer section
lines.append("## DEVELOPER")
if not developer:
lines.append(
f"ERROR: Not initialized. Run: python ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
)
return "\n".join(lines)
lines.append(f"Name: {developer}")
lines.append("")
# Git status
lines.append("## GIT STATUS")
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
branch = branch_out.strip() or "unknown"
lines.append(f"Branch: {branch}")
_, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root)
status_lines = [line for line in status_out.splitlines() if line.strip()]
status_count = len(status_lines)
if status_count == 0:
lines.append("Working directory: Clean")
else:
lines.append(f"Working directory: {status_count} uncommitted change(s)")
lines.append("")
lines.append("Changes:")
_, short_out, _ = run_git(["status", "--short"], cwd=repo_root)
for line in short_out.splitlines()[:10]:
lines.append(line)
lines.append("")
# Recent commits
lines.append("## RECENT COMMITS")
_, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root)
if log_out.strip():
for line in log_out.splitlines():
lines.append(line)
else:
lines.append("(no commits)")
lines.append("")
# Package git repos — independent sub-repositories
_append_package_git_context(lines, _collect_package_git_info(repo_root))
# Current task
lines.append("## CURRENT TASK")
current_task = get_current_task(repo_root)
if current_task:
current_task_dir = repo_root / current_task
source_type, context_key, _ = get_current_task_source(repo_root)
lines.append(f"Path: {current_task}")
lines.append(
f"Source: {source_type}" + (f":{context_key}" if context_key else "")
)
ct = load_task(current_task_dir)
if ct:
lines.append(f"Name: {ct.name}")
lines.append(f"Status: {ct.status}")
lines.append(f"Created: {ct.raw.get('createdAt', 'unknown')}")
if ct.description:
lines.append(f"Description: {ct.description}")
# Check for prd.md
prd_file = current_task_dir / "prd.md"
if prd_file.is_file():
lines.append("")
lines.append("[!] This task has prd.md - read it for task details")
else:
lines.append("(none)")
lines.append("")
# Active tasks
lines.append("## ACTIVE TASKS")
tasks_dir = get_tasks_dir(repo_root)
task_count = 0
# Collect all task data for hierarchy display
all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)}
all_statuses = {name: t.status for name, t in all_tasks.items()}
def _print_task_tree(name: str, indent: int = 0) -> None:
nonlocal task_count
t = all_tasks[name]
progress = children_progress(t.children, all_statuses)
prefix = " " * indent
lines.append(f"{prefix}- {name}/ ({t.status}){progress} @{t.assignee or '-'}")
task_count += 1
for child in t.children:
if child in all_tasks:
_print_task_tree(child, indent + 1)
for dir_name in sorted(all_tasks.keys()):
if not all_tasks[dir_name].parent:
_print_task_tree(dir_name)
if task_count == 0:
lines.append("(no active tasks)")
lines.append(f"Total: {task_count} active task(s)")
lines.append("")
# My tasks
lines.append("## MY TASKS (Assigned to me)")
my_task_count = 0
for t in all_tasks.values():
if t.assignee == developer and t.status != "done":
progress = children_progress(t.children, all_statuses)
lines.append(f"- [{t.priority}] {t.title} ({t.status}){progress}")
my_task_count += 1
if my_task_count == 0:
lines.append("(no tasks assigned to you)")
lines.append("")
# Journal file
lines.append("## JOURNAL FILE")
journal_file = get_active_journal_file(repo_root)
if journal_file:
journal_lines = count_lines(journal_file)
relative = f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
lines.append(f"Active file: {relative}")
lines.append(f"Line count: {journal_lines} / 2000")
if journal_lines > 1800:
lines.append("[!] WARNING: Approaching 2000 line limit!")
else:
lines.append("No journal file found")
lines.append("")
# Packages
packages_text = get_packages_section(repo_root)
if packages_text:
lines.append(packages_text)
lines.append("")
# Paths
lines.append("## PATHS")
lines.append(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/")
lines.append(f"Tasks: {DIR_WORKFLOW}/{DIR_TASKS}/")
lines.append(f"Spec: {DIR_WORKFLOW}/{DIR_SPEC}/")
lines.append("")
lines.append("========================================")
return "\n".join(lines)
# =============================================================================
# Record Mode
# =============================================================================
def get_context_record_json(repo_root: Path | None = None) -> dict:
"""Get record-mode context as a dictionary.
Focused on: my active tasks, git status, current task.
"""
if repo_root is None:
repo_root = get_repo_root()
developer = get_developer(repo_root)
tasks_dir = get_tasks_dir(repo_root)
# Git info
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
branch = branch_out.strip() or "unknown"
_, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root)
git_status_count = len([line for line in status_out.splitlines() if line.strip()])
_, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root)
commits = []
for line in log_out.splitlines():
if line.strip():
parts = line.split(" ", 1)
if len(parts) >= 2:
commits.append({"hash": parts[0], "message": parts[1]})
# My tasks (single pass — collect statuses and filter by assignee)
all_tasks_list = list(iter_active_tasks(tasks_dir))
all_statuses = {t.dir_name: t.status for t in all_tasks_list}
my_tasks = []
for t in all_tasks_list:
if t.assignee == developer:
done = sum(
1 for c in t.children
if all_statuses.get(c) in ("completed", "done")
)
my_tasks.append({
"dir": t.dir_name,
"title": t.title,
"status": t.status,
"priority": t.priority,
"children": list(t.children),
"childrenDone": done,
"parent": t.parent,
"meta": t.meta,
})
# Current task
current_task_info = None
current_task = get_current_task(repo_root)
if current_task:
source_type, context_key, _ = get_current_task_source(repo_root)
ct = load_task(repo_root / current_task)
if ct:
current_task_info = {
"path": current_task,
"name": ct.name,
"status": ct.status,
"source": source_type,
"contextKey": context_key,
}
# Package git repos
pkg_git_info = _collect_package_git_info(repo_root)
result = {
"developer": developer or "",
"git": {
"branch": branch,
"isClean": git_status_count == 0,
"uncommittedChanges": git_status_count,
"recentCommits": commits,
},
"myTasks": my_tasks,
"currentTask": current_task_info,
}
if pkg_git_info:
result["packageGit"] = pkg_git_info
return result
def get_context_text_record(repo_root: Path | None = None) -> str:
"""Get context as formatted text for record-session mode.
Focused output: MY ACTIVE TASKS first (with [!!!] emphasis),
then GIT STATUS, RECENT COMMITS, CURRENT TASK.
"""
if repo_root is None:
repo_root = get_repo_root()
lines: list[str] = []
lines.append("========================================")
lines.append("SESSION CONTEXT (RECORD MODE)")
lines.append("========================================")
lines.append("")
developer = get_developer(repo_root)
if not developer:
lines.append(
f"ERROR: Not initialized. Run: python ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
)
return "\n".join(lines)
# MY ACTIVE TASKS — first and prominent
lines.append(f"## [!!!] MY ACTIVE TASKS (Assigned to {developer})")
lines.append("[!] Review whether any should be archived before recording this session.")
lines.append("")
tasks_dir = get_tasks_dir(repo_root)
my_task_count = 0
# Single pass — collect all tasks and filter by assignee
all_statuses = get_all_statuses(tasks_dir)
for t in iter_active_tasks(tasks_dir):
if t.assignee == developer:
progress = children_progress(t.children, all_statuses)
lines.append(f"- [{t.priority}] {t.title} ({t.status}){progress}{t.dir_name}")
my_task_count += 1
if my_task_count == 0:
lines.append("(no active tasks assigned to you)")
lines.append("")
# GIT STATUS
lines.append("## GIT STATUS")
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
branch = branch_out.strip() or "unknown"
lines.append(f"Branch: {branch}")
_, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root)
status_lines = [line for line in status_out.splitlines() if line.strip()]
status_count = len(status_lines)
if status_count == 0:
lines.append("Working directory: Clean")
else:
lines.append(f"Working directory: {status_count} uncommitted change(s)")
lines.append("")
lines.append("Changes:")
_, short_out, _ = run_git(["status", "--short"], cwd=repo_root)
for line in short_out.splitlines()[:10]:
lines.append(line)
lines.append("")
# RECENT COMMITS
lines.append("## RECENT COMMITS")
_, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root)
if log_out.strip():
for line in log_out.splitlines():
lines.append(line)
else:
lines.append("(no commits)")
lines.append("")
# Package git repos — independent sub-repositories
_append_package_git_context(lines, _collect_package_git_info(repo_root))
# CURRENT TASK
lines.append("## CURRENT TASK")
current_task = get_current_task(repo_root)
if current_task:
source_type, context_key, _ = get_current_task_source(repo_root)
lines.append(f"Path: {current_task}")
lines.append(
f"Source: {source_type}" + (f":{context_key}" if context_key else "")
)
ct = load_task(repo_root / current_task)
if ct:
lines.append(f"Name: {ct.name}")
lines.append(f"Status: {ct.status}")
else:
lines.append("(none)")
lines.append("")
lines.append("========================================")
return "\n".join(lines)
def output_text(repo_root: Path | None = None) -> None:
"""Output context in text format.
Args:
repo_root: Repository root path. Defaults to auto-detected.
"""
print(get_context_text(repo_root))

View File

@@ -0,0 +1,223 @@
#!/usr/bin/env python3
"""
Task JSONL context management.
Provides:
cmd_add_context - Add entry to JSONL context file
cmd_validate - Validate JSONL context files
cmd_list_context - List JSONL context entries
Note:
``cmd_init_context`` was removed in v0.5.0-beta.12. JSONL context files
are now seeded at ``task.py create`` time with a self-describing
``_example`` line; the AI agent curates real entries during Phase 1.3 of
the workflow. See ``.trellis/workflow.md`` Phase 1.3 for the current
instructions.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from .log import Colors, colored
from .paths import get_repo_root
from .task_utils import resolve_task_dir
# =============================================================================
# Command: add-context
# =============================================================================
def cmd_add_context(args: argparse.Namespace) -> int:
"""Add entry to JSONL context file."""
repo_root = get_repo_root()
target_dir = resolve_task_dir(args.dir, repo_root)
jsonl_name = args.file
path = args.path
reason = args.reason or "Added manually"
if not target_dir.is_dir():
print(colored(f"Error: Directory not found: {target_dir}", Colors.RED))
return 1
# Support shorthand
if not jsonl_name.endswith(".jsonl"):
jsonl_name = f"{jsonl_name}.jsonl"
jsonl_file = target_dir / jsonl_name
full_path = repo_root / path
entry_type = "file"
if full_path.is_dir():
entry_type = "directory"
if not path.endswith("/"):
path = f"{path}/"
elif not full_path.is_file():
print(colored(f"Error: Path not found: {path}", Colors.RED))
return 1
# Check if already exists
if jsonl_file.is_file():
content = jsonl_file.read_text(encoding="utf-8")
if f'"{path}"' in content:
print(colored(f"Warning: Entry already exists for {path}", Colors.YELLOW))
return 0
# Add entry
entry: dict
if entry_type == "directory":
entry = {"file": path, "type": "directory", "reason": reason}
else:
entry = {"file": path, "reason": reason}
with jsonl_file.open("a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
print(colored(f"Added {entry_type}: {path}", Colors.GREEN))
return 0
# =============================================================================
# Command: validate
# =============================================================================
def cmd_validate(args: argparse.Namespace) -> int:
"""Validate JSONL context files."""
repo_root = get_repo_root()
target_dir = resolve_task_dir(args.dir, repo_root)
if not target_dir.is_dir():
print(colored("Error: task directory required", Colors.RED))
return 1
print(colored("=== Validating Context Files ===", Colors.BLUE))
print(f"Target dir: {target_dir}")
print()
total_errors = 0
for jsonl_name in ["implement.jsonl", "check.jsonl"]:
jsonl_file = target_dir / jsonl_name
errors = _validate_jsonl(jsonl_file, repo_root)
total_errors += errors
print()
if total_errors == 0:
print(colored("✓ All validations passed", Colors.GREEN))
return 0
else:
print(colored(f"✗ Validation failed ({total_errors} errors)", Colors.RED))
return 1
def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
"""Validate a single JSONL file.
Seed rows (no ``file`` field — typically ``{"_example": "..."}``) are
skipped silently; they are self-describing comments, not real entries.
"""
file_name = jsonl_file.name
errors = 0
if not jsonl_file.is_file():
print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}")
return 0
line_num = 0
real_entries = 0
for line in jsonl_file.read_text(encoding="utf-8").splitlines():
line_num += 1
if not line.strip():
continue
try:
data = json.loads(line)
except json.JSONDecodeError:
print(f" {colored(f'{file_name}:{line_num}: Invalid JSON', Colors.RED)}")
errors += 1
continue
file_path = data.get("file")
entry_type = data.get("type", "file")
if not file_path:
# Seed / comment row — skip silently
continue
real_entries += 1
full_path = repo_root / file_path
if entry_type == "directory":
if not full_path.is_dir():
print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}")
errors += 1
else:
if not full_path.is_file():
print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
errors += 1
if errors == 0:
print(f" {colored(f'{file_name}: ✓ ({real_entries} entries)', Colors.GREEN)}")
else:
print(f" {colored(f'{file_name}: ✗ ({errors} errors)', Colors.RED)}")
return errors
# =============================================================================
# Command: list-context
# =============================================================================
def cmd_list_context(args: argparse.Namespace) -> int:
"""List JSONL context entries."""
repo_root = get_repo_root()
target_dir = resolve_task_dir(args.dir, repo_root)
if not target_dir.is_dir():
print(colored("Error: task directory required", Colors.RED))
return 1
print(colored("=== Context Files ===", Colors.BLUE))
print()
for jsonl_name in ["implement.jsonl", "check.jsonl"]:
jsonl_file = target_dir / jsonl_name
if not jsonl_file.is_file():
continue
print(colored(f"[{jsonl_name}]", Colors.CYAN))
count = 0
seed_only = True
for line in jsonl_file.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
try:
data = json.loads(line)
except json.JSONDecodeError:
continue
file_path = data.get("file")
if not file_path:
# Seed / comment row — don't count as a real entry
continue
seed_only = False
count += 1
entry_type = data.get("type", "file")
reason = data.get("reason", "-")
if entry_type == "directory":
print(f" {colored(f'{count}.', Colors.GREEN)} [DIR] {file_path}")
else:
print(f" {colored(f'{count}.', Colors.GREEN)} {file_path}")
print(f" {colored('', Colors.YELLOW)} {reason}")
if seed_only:
print(f" {colored('(no curated entries yet — only seed row)', Colors.YELLOW)}")
print()
return 0

View File

@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""
Task queue utility functions.
Provides:
list_tasks_by_status - List tasks by status
list_pending_tasks - List tasks with pending status
list_tasks_by_assignee - List tasks by assignee
list_my_tasks - List tasks assigned to current developer
get_task_stats - Get P0/P1/P2/P3 counts
"""
from __future__ import annotations
from pathlib import Path
from .paths import (
get_repo_root,
get_developer,
get_tasks_dir,
)
from .tasks import iter_active_tasks
# =============================================================================
# Internal helper
# =============================================================================
def _task_to_dict(t) -> dict:
"""Convert TaskInfo to the dict format callers expect."""
return {
"priority": t.priority,
"id": t.raw.get("id", ""),
"title": t.title,
"status": t.status,
"assignee": t.assignee or "-",
"dir": t.dir_name,
"children": list(t.children),
"parent": t.parent,
}
# =============================================================================
# Public Functions
# =============================================================================
def list_tasks_by_status(
filter_status: str | None = None,
repo_root: Path | None = None
) -> list[dict]:
"""List tasks by status.
Args:
filter_status: Optional status filter.
repo_root: Repository root path. Defaults to auto-detected.
Returns:
List of task info dicts with keys: priority, id, title, status, assignee.
"""
if repo_root is None:
repo_root = get_repo_root()
tasks_dir = get_tasks_dir(repo_root)
results = []
for t in iter_active_tasks(tasks_dir):
if filter_status and t.status != filter_status:
continue
results.append(_task_to_dict(t))
return results
def list_pending_tasks(repo_root: Path | None = None) -> list[dict]:
"""List pending tasks.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
List of task info dicts.
"""
return list_tasks_by_status("planning", repo_root)
def list_tasks_by_assignee(
assignee: str,
filter_status: str | None = None,
repo_root: Path | None = None
) -> list[dict]:
"""List tasks assigned to a specific developer.
Args:
assignee: Developer name.
filter_status: Optional status filter.
repo_root: Repository root path. Defaults to auto-detected.
Returns:
List of task info dicts.
"""
if repo_root is None:
repo_root = get_repo_root()
tasks_dir = get_tasks_dir(repo_root)
results = []
for t in iter_active_tasks(tasks_dir):
if (t.assignee or "-") != assignee:
continue
if filter_status and t.status != filter_status:
continue
results.append(_task_to_dict(t))
return results
def list_my_tasks(
filter_status: str | None = None,
repo_root: Path | None = None
) -> list[dict]:
"""List tasks assigned to current developer.
Args:
filter_status: Optional status filter.
repo_root: Repository root path. Defaults to auto-detected.
Returns:
List of task info dicts.
Raises:
ValueError: If developer not set.
"""
if repo_root is None:
repo_root = get_repo_root()
developer = get_developer(repo_root)
if not developer:
raise ValueError("Developer not set")
return list_tasks_by_assignee(developer, filter_status, repo_root)
def get_task_stats(repo_root: Path | None = None) -> dict[str, int]:
"""Get task statistics.
Args:
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Dict with keys: P0, P1, P2, P3, Total.
"""
if repo_root is None:
repo_root = get_repo_root()
tasks_dir = get_tasks_dir(repo_root)
stats = {"P0": 0, "P1": 0, "P2": 0, "P3": 0, "Total": 0}
for t in iter_active_tasks(tasks_dir):
if t.priority in stats:
stats[t.priority] += 1
stats["Total"] += 1
return stats
def format_task_stats(stats: dict[str, int]) -> str:
"""Format task stats as string.
Args:
stats: Stats dict from get_task_stats.
Returns:
Formatted string like "P0:0 P1:1 P2:2 P3:0 Total:3".
"""
return f"P0:{stats['P0']} P1:{stats['P1']} P2:{stats['P2']} P3:{stats['P3']} Total:{stats['Total']}"
# =============================================================================
# Main Entry (for testing)
# =============================================================================
if __name__ == "__main__":
stats = get_task_stats()
print(format_task_stats(stats))
print()
print("Pending tasks:")
for task in list_pending_tasks():
print(f" {task['priority']}|{task['id']}|{task['title']}|{task['status']}|{task['assignee']}")

View File

@@ -0,0 +1,600 @@
#!/usr/bin/env python3
"""
Task CRUD operations.
Provides:
ensure_tasks_dir - Ensure tasks directory exists
cmd_create - Create a new task
cmd_archive - Archive completed task
cmd_set_branch - Set git branch for task
cmd_set_base_branch - Set PR target branch
cmd_set_scope - Set scope for PR title
cmd_add_subtask - Link child task to parent
cmd_remove_subtask - Unlink child task from parent
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime
from pathlib import Path
from .config import (
get_packages,
is_monorepo,
resolve_package,
validate_package,
)
from .git import run_git
from .io import read_json, write_json
from .log import Colors, colored
from .paths import (
DIR_ARCHIVE,
DIR_TASKS,
DIR_WORKFLOW,
FILE_TASK_JSON,
generate_task_date_prefix,
get_developer,
get_repo_root,
get_tasks_dir,
)
from .task_utils import (
archive_task_complete,
find_task_by_name,
resolve_task_dir,
run_task_hooks,
)
# =============================================================================
# Helper Functions
# =============================================================================
def _slugify(title: str) -> str:
"""Convert title to slug (only works with ASCII)."""
result = title.lower()
result = re.sub(r"[^a-z0-9]", "-", result)
result = re.sub(r"-+", "-", result)
result = result.strip("-")
return result
def ensure_tasks_dir(repo_root: Path) -> Path:
"""Ensure tasks directory exists."""
tasks_dir = get_tasks_dir(repo_root)
archive_dir = tasks_dir / "archive"
if not tasks_dir.exists():
tasks_dir.mkdir(parents=True)
print(colored(f"Created tasks directory: {tasks_dir}", Colors.GREEN), file=sys.stderr)
if not archive_dir.exists():
archive_dir.mkdir(parents=True)
return tasks_dir
# =============================================================================
# Sub-agent platform detection + JSONL seeding
# =============================================================================
# Config directories of platforms that consume implement.jsonl / check.jsonl.
# Keep in sync with src/types/ai-tools.ts AI_TOOLS entries — these are the
# platforms listed in workflow.md's "agent-capable" Skill Routing block
# (Class-1 hook-inject + Class-2 pull-based preludes). Kilo / Antigravity /
# Windsurf are NOT in this list: they do not consume JSONL.
_SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
".claude",
".cursor",
".codex",
".kiro",
".gemini",
".opencode",
".qoder",
".codebuddy",
".factory", # Factory Droid
".github/copilot",
".pi", # Pi Agent
)
_SEED_EXAMPLE = (
"Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. "
"Put spec/research files only — no code paths. "
"Run `python .trellis/scripts/get_context.py --mode packages` to list available specs. "
"Delete this line once real entries are added."
)
def _has_subagent_platform(repo_root: Path) -> bool:
"""Return True if any sub-agent-capable platform is configured.
Detected by probing well-known config directories at the repo root. Used
only to decide whether ``task.py create`` should seed empty
``implement.jsonl`` / ``check.jsonl`` files.
"""
for config_dir in _SUBAGENT_CONFIG_DIRS:
if (repo_root / config_dir).is_dir():
return True
return False
def _write_seed_jsonl(path: Path) -> None:
"""Write a one-line seed JSONL file with a self-describing ``_example``.
The seed row has no ``file`` field, so downstream consumers (hooks +
preludes) that iterate entries via ``item.get("file")`` naturally skip
it. The row exists purely as an in-file prompt for the AI curator.
"""
seed = {"_example": _SEED_EXAMPLE}
path.write_text(json.dumps(seed, ensure_ascii=False) + "\n", encoding="utf-8")
# =============================================================================
# Command: create
# =============================================================================
def cmd_create(args: argparse.Namespace) -> int:
"""Create a new task."""
repo_root = get_repo_root()
if not args.title:
print(colored("Error: title is required", Colors.RED), file=sys.stderr)
return 1
# Validate --package (CLI source: fail-fast)
package: str | None = getattr(args, "package", None)
if not is_monorepo(repo_root):
# Single-repo: ignore --package, no package prefix
if package:
print(colored(f"Warning: --package ignored in single-repo project", Colors.YELLOW), file=sys.stderr)
package = None
elif package:
if not validate_package(package, repo_root):
packages = get_packages(repo_root)
available = ", ".join(sorted(packages.keys())) if packages else "(none)"
print(colored(f"Error: unknown package '{package}'. Available: {available}", Colors.RED), file=sys.stderr)
return 1
else:
# Inferred: default_package → None (no task.json yet for create)
package = resolve_package(repo_root=repo_root)
# Default assignee to current developer
assignee = args.assignee
if not assignee:
assignee = get_developer(repo_root)
if not assignee:
print(colored("Error: No developer set. Run init_developer.py first or use --assignee", Colors.RED), file=sys.stderr)
return 1
ensure_tasks_dir(repo_root)
# Get current developer as creator
creator = get_developer(repo_root) or assignee
# Generate slug if not provided
slug = args.slug or _slugify(args.title)
if not slug:
print(colored("Error: could not generate slug from title", Colors.RED), file=sys.stderr)
return 1
# Create task directory with MM-DD-slug format
tasks_dir = get_tasks_dir(repo_root)
date_prefix = generate_task_date_prefix()
dir_name = f"{date_prefix}-{slug}"
task_dir = tasks_dir / dir_name
task_json_path = task_dir / FILE_TASK_JSON
if task_dir.exists():
print(colored(f"Warning: Task directory already exists: {dir_name}", Colors.YELLOW), file=sys.stderr)
else:
task_dir.mkdir(parents=True)
today = datetime.now().strftime("%Y-%m-%d")
# Record current branch as base_branch (PR target)
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
current_branch = branch_out.strip() or "main"
task_data = {
"id": slug,
"name": slug,
"title": args.title,
"description": args.description or "",
"status": "planning",
"dev_type": None,
"scope": None,
"package": package,
"priority": args.priority,
"creator": creator,
"assignee": assignee,
"createdAt": today,
"completedAt": None,
"branch": None,
"base_branch": current_branch,
"worktree_path": None,
"commit": None,
"pr_url": None,
"subtasks": [],
"children": [],
"parent": None,
"relatedFiles": [],
"notes": "",
"meta": {},
}
write_json(task_json_path, task_data)
# Seed implement.jsonl / check.jsonl for sub-agent-capable platforms.
# Agent curates real entries in Phase 1.3 (see .trellis/workflow.md).
# Agent-less platforms (Kilo / Antigravity / Windsurf) skip this — they
# load specs via the trellis-before-dev skill instead of JSONL.
seeded_jsonl = False
if _has_subagent_platform(repo_root):
for jsonl_name in ("implement.jsonl", "check.jsonl"):
jsonl_path = task_dir / jsonl_name
if not jsonl_path.exists():
_write_seed_jsonl(jsonl_path)
seeded_jsonl = True
# Handle --parent: establish bidirectional link
if args.parent:
parent_dir = resolve_task_dir(args.parent, repo_root)
parent_json_path = parent_dir / FILE_TASK_JSON
if not parent_json_path.is_file():
print(colored(f"Warning: Parent task.json not found: {args.parent}", Colors.YELLOW), file=sys.stderr)
else:
parent_data = read_json(parent_json_path)
if parent_data:
# Add child to parent's children list
parent_children = parent_data.get("children", [])
if dir_name not in parent_children:
parent_children.append(dir_name)
parent_data["children"] = parent_children
write_json(parent_json_path, parent_data)
# Set parent in child's task.json
task_data["parent"] = parent_dir.name
write_json(task_json_path, task_data)
print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr)
# Auto-activate the new task so the per-turn breadcrumb fires planning
# state. Best-effort: gracefully degrade if no session identity (CLI run
# outside an AI session) — the task is still created, the user can run
# task.py start later. Pointer is session-scoped so this never affects
# other AI sessions.
try:
from .active_task import resolve_context_key, set_active_task
if resolve_context_key():
try:
rel_dir = task_dir.relative_to(repo_root).as_posix()
except ValueError:
rel_dir = str(task_dir)
set_active_task(rel_dir, repo_root)
except Exception:
pass
print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
print("", file=sys.stderr)
print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
print(" 1. Create prd.md with requirements", file=sys.stderr)
if seeded_jsonl:
print(
" 2. Curate implement.jsonl / check.jsonl (spec + research files only — "
"see .trellis/workflow.md Phase 1.3)",
file=sys.stderr,
)
print(" 3. Run: python task.py start <dir>", file=sys.stderr)
else:
print(" 2. Run: python task.py start <dir>", file=sys.stderr)
print("", file=sys.stderr)
# Output relative path for script chaining
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}")
run_task_hooks("after_create", task_json_path, repo_root)
return 0
# =============================================================================
# Command: archive
# =============================================================================
def cmd_archive(args: argparse.Namespace) -> int:
"""Archive completed task."""
repo_root = get_repo_root()
task_name = args.name
if not task_name:
print(colored("Error: Task name is required", Colors.RED), file=sys.stderr)
return 1
tasks_dir = get_tasks_dir(repo_root)
# Resolve task directory (supports task name, relative path, or absolute path)
task_dir = resolve_task_dir(task_name, repo_root)
if not task_dir or not task_dir.is_dir():
print(colored(f"Error: Task not found: {task_name}", Colors.RED), file=sys.stderr)
print("Active tasks:", file=sys.stderr)
# Import lazily to avoid circular dependency
from .tasks import iter_active_tasks
for t in iter_active_tasks(tasks_dir):
print(f" - {t.dir_name}/", file=sys.stderr)
return 1
dir_name = task_dir.name
task_json_path = task_dir / FILE_TASK_JSON
# Update status before archiving
today = datetime.now().strftime("%Y-%m-%d")
if task_json_path.is_file():
data = read_json(task_json_path)
if data:
data["status"] = "completed"
data["completedAt"] = today
write_json(task_json_path, data)
# Handle subtask relationships on archive.
# Keep this task in its parent's children list so progress
# counters (children_progress) stay consistent — children
# missing from the active set are treated as completed.
task_children = data.get("children", [])
# If this is a parent, clear parent field in all children
if task_children:
for child_name in task_children:
child_dir_path = find_task_by_name(child_name, tasks_dir)
if child_dir_path:
child_json = child_dir_path / FILE_TASK_JSON
if child_json.is_file():
child_data = read_json(child_json)
if child_data:
child_data["parent"] = None
write_json(child_json, child_data)
# Clear any session that still points at this task before the path moves.
from .active_task import clear_task_from_sessions
clear_task_from_sessions(str(task_dir), repo_root)
# Archive
result = archive_task_complete(task_dir, repo_root)
if "archived_to" in result:
archive_dest = Path(result["archived_to"])
year_month = archive_dest.parent.name
print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
# Auto-commit unless --no-commit
if not getattr(args, "no_commit", False):
_auto_commit_archive(dir_name, repo_root)
# Return the archive path
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}")
# Run hooks with the archived path
archived_json = archive_dest / FILE_TASK_JSON
run_task_hooks("after_archive", archived_json, repo_root)
return 0
return 1
def _auto_commit_archive(task_name: str, repo_root: Path) -> None:
"""Stage .trellis/tasks/ changes and commit after archive."""
tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
run_git(["add", "-A", tasks_rel], cwd=repo_root)
# Check if there are staged changes
rc, _, _ = run_git(
["diff", "--cached", "--quiet", "--", tasks_rel], cwd=repo_root
)
if rc == 0:
print("[OK] No task changes to commit.", file=sys.stderr)
return
commit_msg = f"chore(task): archive {task_name}"
rc, _, err = run_git(["commit", "-m", commit_msg], cwd=repo_root)
if rc == 0:
print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
else:
print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr)
# =============================================================================
# Command: add-subtask
# =============================================================================
def cmd_add_subtask(args: argparse.Namespace) -> int:
"""Link a child task to a parent task."""
repo_root = get_repo_root()
parent_dir = resolve_task_dir(args.parent_dir, repo_root)
child_dir = resolve_task_dir(args.child_dir, repo_root)
parent_json_path = parent_dir / FILE_TASK_JSON
child_json_path = child_dir / FILE_TASK_JSON
if not parent_json_path.is_file():
print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
return 1
if not child_json_path.is_file():
print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
return 1
parent_data = read_json(parent_json_path)
child_data = read_json(child_json_path)
if not parent_data or not child_data:
print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr)
return 1
# Check if child already has a parent
existing_parent = child_data.get("parent")
if existing_parent:
print(colored(f"Error: Child task already has a parent: {existing_parent}", Colors.RED), file=sys.stderr)
return 1
# Add child to parent's children list
parent_children = parent_data.get("children", [])
child_dir_name = child_dir.name
if child_dir_name not in parent_children:
parent_children.append(child_dir_name)
parent_data["children"] = parent_children
# Set parent in child's task.json
child_data["parent"] = parent_dir.name
# Write both
write_json(parent_json_path, parent_data)
write_json(child_json_path, child_data)
print(colored(f"Linked: {child_dir.name} -> {parent_dir.name}", Colors.GREEN), file=sys.stderr)
return 0
# =============================================================================
# Command: remove-subtask
# =============================================================================
def cmd_remove_subtask(args: argparse.Namespace) -> int:
"""Unlink a child task from a parent task."""
repo_root = get_repo_root()
parent_dir = resolve_task_dir(args.parent_dir, repo_root)
child_dir = resolve_task_dir(args.child_dir, repo_root)
parent_json_path = parent_dir / FILE_TASK_JSON
child_json_path = child_dir / FILE_TASK_JSON
if not parent_json_path.is_file():
print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
return 1
if not child_json_path.is_file():
print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
return 1
parent_data = read_json(parent_json_path)
child_data = read_json(child_json_path)
if not parent_data or not child_data:
print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr)
return 1
# Remove child from parent's children list
parent_children = parent_data.get("children", [])
child_dir_name = child_dir.name
if child_dir_name in parent_children:
parent_children.remove(child_dir_name)
parent_data["children"] = parent_children
# Clear parent in child's task.json
child_data["parent"] = None
# Write both
write_json(parent_json_path, parent_data)
write_json(child_json_path, child_data)
print(colored(f"Unlinked: {child_dir.name} from {parent_dir.name}", Colors.GREEN), file=sys.stderr)
return 0
# =============================================================================
# Command: set-branch
# =============================================================================
def cmd_set_branch(args: argparse.Namespace) -> int:
"""Set git branch for task."""
repo_root = get_repo_root()
target_dir = resolve_task_dir(args.dir, repo_root)
branch = args.branch
if not branch:
print(colored("Error: Missing arguments", Colors.RED))
print("Usage: python task.py set-branch <task-dir> <branch-name>")
return 1
task_json = target_dir / FILE_TASK_JSON
if not task_json.is_file():
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
return 1
data = read_json(task_json)
if not data:
return 1
data["branch"] = branch
write_json(task_json, data)
print(colored(f"✓ Branch set to: {branch}", Colors.GREEN))
return 0
# =============================================================================
# Command: set-base-branch
# =============================================================================
def cmd_set_base_branch(args: argparse.Namespace) -> int:
"""Set the base branch (PR target) for task."""
repo_root = get_repo_root()
target_dir = resolve_task_dir(args.dir, repo_root)
base_branch = args.base_branch
if not base_branch:
print(colored("Error: Missing arguments", Colors.RED))
print("Usage: python task.py set-base-branch <task-dir> <base-branch>")
print("Example: python task.py set-base-branch <dir> develop")
print()
print("This sets the target branch for PR (the branch your feature will merge into).")
return 1
task_json = target_dir / FILE_TASK_JSON
if not task_json.is_file():
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
return 1
data = read_json(task_json)
if not data:
return 1
data["base_branch"] = base_branch
write_json(task_json, data)
print(colored(f"✓ Base branch set to: {base_branch}", Colors.GREEN))
print(f" PR will target: {base_branch}")
return 0
# =============================================================================
# Command: set-scope
# =============================================================================
def cmd_set_scope(args: argparse.Namespace) -> int:
"""Set scope for PR title."""
repo_root = get_repo_root()
target_dir = resolve_task_dir(args.dir, repo_root)
scope = args.scope
if not scope:
print(colored("Error: Missing arguments", Colors.RED))
print("Usage: python task.py set-scope <task-dir> <scope>")
return 1
task_json = target_dir / FILE_TASK_JSON
if not task_json.is_file():
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
return 1
data = read_json(task_json)
if not data:
return 1
data["scope"] = scope
write_json(task_json, data)
print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
return 0

View File

@@ -0,0 +1,274 @@
#!/usr/bin/env python3
"""
Task utility functions.
Provides:
is_safe_task_path - Validate task path is safe to operate on
find_task_by_name - Find task directory by name
resolve_task_dir - Resolve task directory from name, relative, or absolute path
archive_task_dir - Archive task to monthly directory
run_task_hooks - Run lifecycle hooks for task events
"""
from __future__ import annotations
import shutil
import sys
from datetime import datetime
from pathlib import Path
from .paths import get_repo_root, get_tasks_dir
# =============================================================================
# Path Safety
# =============================================================================
def is_safe_task_path(task_path: str, repo_root: Path | None = None) -> bool:
"""Check if a relative task path is safe to operate on.
Args:
task_path: Task path (relative to repo_root).
repo_root: Repository root path. Defaults to auto-detected.
Returns:
True if safe, False if dangerous.
"""
if repo_root is None:
repo_root = get_repo_root()
normalized = task_path.replace("\\", "/")
# Check empty or null
if not normalized or normalized == "null":
print("Error: empty or null task path", file=sys.stderr)
return False
# Reject absolute paths
if Path(task_path).is_absolute():
print(f"Error: absolute path not allowed: {task_path}", file=sys.stderr)
return False
# Reject ".", "..", paths starting with "./" or "../", or containing ".."
if normalized in (".", "..") or normalized.startswith("./") or normalized.startswith("../") or ".." in normalized:
print(f"Error: path traversal not allowed: {task_path}", file=sys.stderr)
return False
# Final check: ensure resolved path is not the repo root
abs_path = repo_root / Path(normalized)
if abs_path.exists():
try:
resolved = abs_path.resolve()
root_resolved = repo_root.resolve()
if resolved == root_resolved:
print(f"Error: path resolves to repo root: {task_path}", file=sys.stderr)
return False
except (OSError, IOError):
pass
return True
# =============================================================================
# Task Lookup
# =============================================================================
def find_task_by_name(task_name: str, tasks_dir: Path) -> Path | None:
"""Find task directory by name (exact or suffix match).
Args:
task_name: Task name to find.
tasks_dir: Tasks directory path.
Returns:
Absolute path to task directory, or None if not found.
"""
if not task_name or not tasks_dir or not tasks_dir.is_dir():
return None
# Try exact match first
exact_match = tasks_dir / task_name
if exact_match.is_dir():
return exact_match
# Try suffix match (e.g., "my-task" matches "01-21-my-task")
for d in tasks_dir.iterdir():
if d.is_dir() and d.name.endswith(f"-{task_name}"):
return d
return None
# =============================================================================
# Archive Operations
# =============================================================================
def archive_task_dir(task_dir_abs: Path, repo_root: Path | None = None) -> Path | None:
"""Archive a task directory to archive/{YYYY-MM}/.
Args:
task_dir_abs: Absolute path to task directory.
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Path to archived directory, or None on error.
"""
if not task_dir_abs.is_dir():
print(f"Error: task directory not found: {task_dir_abs}", file=sys.stderr)
return None
# Get tasks directory (parent of the task)
tasks_dir = task_dir_abs.parent
archive_dir = tasks_dir / "archive"
year_month = datetime.now().strftime("%Y-%m")
month_dir = archive_dir / year_month
# Create archive directory
try:
month_dir.mkdir(parents=True, exist_ok=True)
except (OSError, IOError) as e:
print(f"Error: Failed to create archive directory: {e}", file=sys.stderr)
return None
# Move task to archive
task_name = task_dir_abs.name
dest = month_dir / task_name
try:
shutil.move(str(task_dir_abs), str(dest))
except (OSError, IOError, shutil.Error) as e:
print(f"Error: Failed to move task to archive: {e}", file=sys.stderr)
return None
return dest
def archive_task_complete(
task_dir_abs: Path,
repo_root: Path | None = None
) -> dict[str, str]:
"""Complete archive workflow: archive directory.
Args:
task_dir_abs: Absolute path to task directory.
repo_root: Repository root path. Defaults to auto-detected.
Returns:
Dict with archive result info.
"""
if not task_dir_abs.is_dir():
print(f"Error: task directory not found: {task_dir_abs}", file=sys.stderr)
return {}
archive_dest = archive_task_dir(task_dir_abs, repo_root)
if archive_dest:
return {"archived_to": str(archive_dest)}
return {}
# =============================================================================
# Task Directory Resolution
# =============================================================================
def resolve_task_dir(target_dir: str, repo_root: Path) -> Path:
"""Resolve task directory to absolute path.
Supports:
- Absolute path: /path/to/task
- Relative path: .trellis/tasks/01-31-my-task
- Task name: my-task (uses find_task_by_name for lookup)
Args:
target_dir: Task directory specification.
repo_root: Repository root path.
Returns:
Resolved absolute path.
"""
if not target_dir:
return Path()
normalized = target_dir.replace("\\", "/")
while normalized.startswith("./"):
normalized = normalized[2:]
# Absolute path
if Path(target_dir).is_absolute():
return Path(target_dir)
# Relative path (contains path separator or starts with .trellis)
if "/" in normalized or normalized.startswith(".trellis"):
return repo_root / Path(normalized)
# Task name - try to find in tasks directory
tasks_dir = get_tasks_dir(repo_root)
found = find_task_by_name(target_dir, tasks_dir)
if found:
return found
# Fallback to treating as relative path
return repo_root / Path(normalized)
# =============================================================================
# Lifecycle Hooks
# =============================================================================
def run_task_hooks(event: str, task_json_path: Path, repo_root: Path) -> None:
"""Run lifecycle hooks for a task event.
Args:
event: Event name (e.g. "after_create").
task_json_path: Absolute path to the task's task.json.
repo_root: Repository root for cwd and config lookup.
"""
import os
import subprocess
from .config import get_hooks
from .log import Colors, colored
commands = get_hooks(event, repo_root)
if not commands:
return
env = {**os.environ, "TASK_JSON_PATH": str(task_json_path)}
for cmd in commands:
try:
result = subprocess.run(
cmd,
shell=True,
cwd=repo_root,
env=env,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
if result.returncode != 0:
print(
colored(f"[WARN] Hook failed ({event}): {cmd}", Colors.YELLOW),
file=sys.stderr,
)
if result.stderr.strip():
print(f" {result.stderr.strip()}", file=sys.stderr)
except Exception as e:
print(
colored(f"[WARN] Hook error ({event}): {cmd}{e}", Colors.YELLOW),
file=sys.stderr,
)
# =============================================================================
# Main Entry (for testing)
# =============================================================================
if __name__ == "__main__":
repo = get_repo_root()
tasks = get_tasks_dir(repo)
print(f"Tasks dir: {tasks}")
print(f"is_safe_task_path('.trellis/tasks/test'): {is_safe_task_path('.trellis/tasks/test', repo)}")
print(f"is_safe_task_path('../test'): {is_safe_task_path('../test', repo)}")

View File

@@ -0,0 +1,112 @@
"""
Task data access layer.
Single source of truth for loading and iterating task directories.
Replaces scattered task.json parsing across 9+ files.
Provides:
load_task — Load a single task by directory path
iter_active_tasks — Iterate all non-archived tasks (sorted)
get_all_statuses — Get {dir_name: status} map for children progress
"""
from __future__ import annotations
from collections.abc import Iterator
from pathlib import Path
from .io import read_json
from .paths import FILE_TASK_JSON
from .types import TaskInfo
def load_task(task_dir: Path) -> TaskInfo | None:
"""Load task from a directory containing task.json.
Args:
task_dir: Absolute path to the task directory.
Returns:
TaskInfo if task.json exists and is valid, None otherwise.
"""
task_json = task_dir / FILE_TASK_JSON
if not task_json.is_file():
return None
data = read_json(task_json)
if not data:
return None
return TaskInfo(
dir_name=task_dir.name,
directory=task_dir,
title=data.get("title") or data.get("name") or "unknown",
status=data.get("status", "unknown"),
assignee=data.get("assignee", ""),
priority=data.get("priority", "P2"),
children=tuple(data.get("children", [])),
parent=data.get("parent"),
package=data.get("package"),
raw=data,
)
def iter_active_tasks(tasks_dir: Path) -> Iterator[TaskInfo]:
"""Iterate all active (non-archived) tasks, sorted by directory name.
Skips the "archive" directory and directories without valid task.json.
Args:
tasks_dir: Path to the tasks directory.
Yields:
TaskInfo for each valid task.
"""
if not tasks_dir.is_dir():
return
for d in sorted(tasks_dir.iterdir()):
if not d.is_dir() or d.name == "archive":
continue
info = load_task(d)
if info is not None:
yield info
def get_all_statuses(tasks_dir: Path) -> dict[str, str]:
"""Get a {dir_name: status} mapping for all active tasks.
Useful for computing children progress without loading full TaskInfo.
Args:
tasks_dir: Path to the tasks directory.
Returns:
Dict mapping directory names to status strings.
"""
return {t.dir_name: t.status for t in iter_active_tasks(tasks_dir)}
def children_progress(
children: tuple[str, ...] | list[str],
all_statuses: dict[str, str],
) -> str:
"""Format children progress string like " [2/3 done]".
Args:
children: List of child directory names.
all_statuses: Status map from get_all_statuses().
Returns:
Formatted string, or "" if no children.
"""
if not children:
return ""
# A child missing from active statuses has been archived (cmd_archive
# sets status=completed before moving the dir). Count it as done so
# parent progress doesn't regress when children are archived.
done = sum(
1 for c in children
if c not in all_statuses or all_statuses.get(c) in ("completed", "done")
)
return f" [{done}/{len(children)} done]"

View File

@@ -0,0 +1,110 @@
"""
Core type definitions for Trellis task data.
Provides:
TaskData — TypedDict for task.json shape (read-path type hints only)
TaskInfo — Frozen dataclass for loaded task (the public API type)
AgentRecord — TypedDict for registry.json agent entries
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import TypedDict
# =============================================================================
# task.json shape (TypedDict — used only for read-path type hints)
# =============================================================================
class TaskData(TypedDict, total=False):
"""Shape of task.json on disk.
Used only for type annotations when reading task.json.
Writes must use the original dict to avoid losing unknown fields.
"""
id: str
name: str
title: str
description: str
status: str
dev_type: str
scope: str | None
package: str | None
priority: str
creator: str
assignee: str
createdAt: str
completedAt: str | None
branch: str | None
base_branch: str | None
worktree_path: str | None
commit: str | None
pr_url: str | None
subtasks: list[str]
children: list[str]
parent: str | None
relatedFiles: list[str]
notes: str
meta: dict
# =============================================================================
# Loaded task object (frozen dataclass — the public API type)
# =============================================================================
@dataclass(frozen=True)
class TaskInfo:
"""Immutable view of a loaded task.
Created by load_task() / iter_active_tasks().
Contains the commonly accessed fields; the original dict
is preserved in `raw` for write-back and uncommon field access.
"""
dir_name: str
directory: Path
title: str
status: str
assignee: str
priority: str
children: tuple[str, ...]
parent: str | None
package: str | None
raw: dict # original dict — use for writes and uncommon fields
@property
def name(self) -> str:
"""Task name (id or name field)."""
return self.raw.get("name") or self.raw.get("id") or self.dir_name
@property
def description(self) -> str:
return self.raw.get("description", "")
@property
def branch(self) -> str | None:
return self.raw.get("branch")
@property
def meta(self) -> dict:
return self.raw.get("meta", {})
# =============================================================================
# registry.json agent entry
# =============================================================================
class AgentRecord(TypedDict, total=False):
"""Shape of an agent entry in registry.json."""
id: str
pid: int
task_dir: str
worktree_path: str
branch: str
platform: str
started_at: str
status: str

View File

@@ -0,0 +1,188 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Workflow Phase Extraction.
Extracts step-level content from .trellis/workflow.md and optionally filters
platform-specific blocks.
Platform marker syntax in workflow.md:
[Claude Code, Cursor, ...]
agent-capable content
[/Claude Code, Cursor, ...]
Provides:
get_phase_index - Extract the Phase Index section (no --step)
get_step - Extract a single step (#### X.X) section
filter_platform - Strip platform blocks that don't include the given name
"""
from __future__ import annotations
import re
from .paths import DIR_WORKFLOW, get_repo_root
def _workflow_md_path():
return get_repo_root() / DIR_WORKFLOW / "workflow.md"
# Match a line that *is* a platform marker: "[A, B, C]" or "[/A, B, C]"
_MARKER_RE = re.compile(r"^\[(/?)([A-Za-z][^\[\]]*)\]\s*$")
# Step heading: "#### 1.0 Title" or "#### 1.0 ..."
_STEP_HEADING_RE = re.compile(r"^####\s+(\d+\.\d+)\b.*$")
# Phase Index starts here; Phase 1/2/3 step bodies follow; ends at Breadcrumbs.
_PHASE_INDEX_HEADING = "## Phase Index"
def _read_workflow() -> str:
path = _workflow_md_path()
if not path.exists():
raise FileNotFoundError(f"workflow.md not found: {path}")
return path.read_text(encoding="utf-8")
def _parse_marker(line: str) -> tuple[bool, list[str]] | None:
"""Parse a platform marker line.
Returns:
(is_closing, [platform_names]) if line is a marker, else None.
"""
m = _MARKER_RE.match(line)
if not m:
return None
is_closing = m.group(1) == "/"
names = [p.strip() for p in m.group(2).split(",") if p.strip()]
return is_closing, names
def get_phase_index() -> str:
"""Return Phase Index + Phase 1/2/3 step bodies from workflow.md.
Matches what the SessionStart hook injects into the `<workflow>` block:
starts at `## Phase Index`, continues through `## Phase 1: Plan`,
`## Phase 2: Execute`, `## Phase 3: Finish`, stops at
`## Customizing Trellis (for forks)` (the docs-for-forks footer).
`[workflow-state:STATUS]` tag blocks (now embedded in Phase Index since
v0.5.0-rc.0) are consumed by the UserPromptSubmit hook so they're
stripped from this output.
"""
text = _read_workflow()
lines = text.splitlines()
start: int | None = None
end: int | None = None
for i, line in enumerate(lines):
stripped = line.strip()
if start is None and stripped == _PHASE_INDEX_HEADING:
start = i
continue
if start is not None and stripped == "## Customizing Trellis (for forks)":
end = i
break
if start is None:
return ""
if end is None:
end = len(lines)
section = "\n".join(lines[start:end]).rstrip()
# Strip [workflow-state:STATUS]...[/workflow-state:STATUS] blocks since
# they're injected separately by inject-workflow-state.py per-turn.
import re as _re
tag_re = _re.compile(
r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]\n?",
_re.DOTALL,
)
return tag_re.sub("", section).rstrip() + "\n"
def get_step(step_id: str) -> str:
"""Return the `#### X.X` section matching step_id (header + body).
Body ends at the next `####` or `---` or `##` heading (whichever comes first).
"""
text = _read_workflow()
lines = text.splitlines()
start: int | None = None
for i, line in enumerate(lines):
m = _STEP_HEADING_RE.match(line)
if m and m.group(1) == step_id:
start = i
break
if start is None:
return ""
end: int = len(lines)
for j in range(start + 1, len(lines)):
line = lines[j]
if line.startswith("#### "):
end = j
break
if line.startswith("## "):
end = j
break
# Horizontal rule at column 0
if line.strip() == "---":
end = j
break
return "\n".join(lines[start:end]).rstrip() + "\n"
def _platform_matches(platform: str, block_names: list[str]) -> bool:
"""Case-insensitive fuzzy match: accept 'cursor', 'Cursor', 'claude-code', 'Claude Code'."""
needle = platform.lower().replace("-", "").replace("_", "").replace(" ", "")
for name in block_names:
hay = name.lower().replace("-", "").replace("_", "").replace(" ", "")
if needle == hay:
return True
return False
def filter_platform(content: str, platform: str) -> str:
"""Keep lines outside any `[...]` block + lines inside blocks that include platform.
Marker lines themselves are dropped from the output.
"""
lines = content.splitlines()
out: list[str] = []
in_block = False
keep_block = False
for line in lines:
marker = _parse_marker(line)
if marker is not None:
is_closing, names = marker
if not is_closing:
in_block = True
keep_block = _platform_matches(platform, names)
else:
in_block = False
keep_block = False
continue # drop the marker line itself
if in_block:
if keep_block:
out.append(line)
continue
out.append(line)
# Collapse runs of 3+ blank lines that may arise from dropped markers
collapsed: list[str] = []
blank_run = 0
for line in out:
if line.strip() == "":
blank_run += 1
if blank_run <= 2:
collapsed.append(line)
else:
blank_run = 0
collapsed.append(line)
return "\n".join(collapsed).rstrip() + "\n"

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env python3
"""
Get Session Context for AI Agent.
Usage:
python get_context.py Output context in text format
python get_context.py --json Output context in JSON format
"""
from __future__ import annotations
from common.git_context import main
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More