init with trellis
This commit is contained in:
commit
834a240fe3
146
.opencode/agents/check.md
Normal file
146
.opencode/agents/check.md
Normal file
@ -0,0 +1,146 @@
|
||||
---
|
||||
description: |
|
||||
Code quality check expert. Reviews code changes against specs and self-fixes issues.
|
||||
mode: subagent
|
||||
permission:
|
||||
read: allow
|
||||
write: allow
|
||||
edit: allow
|
||||
bash: allow
|
||||
glob: allow
|
||||
grep: allow
|
||||
mcp__exa__*: allow
|
||||
---
|
||||
# Check Agent
|
||||
|
||||
You are the Check Agent in the Trellis workflow.
|
||||
|
||||
## Context Self-Loading
|
||||
|
||||
**If you see "# Check Agent Task" header with pre-loaded context above, skip this section.**
|
||||
|
||||
Otherwise, load context yourself:
|
||||
|
||||
1. Read `.trellis/.current-task` → get task directory (e.g., `.trellis/tasks/xxx`)
|
||||
2. Read `{task_dir}/check.jsonl` (or `spec.jsonl` as fallback)
|
||||
3. For each entry in JSONL:
|
||||
- If `path` is a file → Read it
|
||||
- If `path` is a directory → Read all `.md` files in it
|
||||
4. Read `{task_dir}/prd.md` for requirements understanding
|
||||
5. Read `.opencode/commands/trellis/finish-work.md` for checklist
|
||||
|
||||
Then proceed with the workflow below using the loaded context.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## Completion Markers (Ralph Loop)
|
||||
|
||||
**CRITICAL**: You are in a loop controlled by the Ralph Loop system.
|
||||
The loop will NOT stop until you output ALL required completion markers.
|
||||
|
||||
Completion markers are generated from `check.jsonl` in the task directory.
|
||||
Each entry's `reason` field becomes a marker: `{REASON}_FINISH`
|
||||
|
||||
For example, if check.jsonl contains:
|
||||
```json
|
||||
{"file": "...", "reason": "TypeCheck"}
|
||||
{"file": "...", "reason": "Lint"}
|
||||
{"file": "...", "reason": "CodeReview"}
|
||||
```
|
||||
|
||||
You MUST output these markers when each check passes:
|
||||
- `TYPECHECK_FINISH` - After typecheck passes
|
||||
- `LINT_FINISH` - After lint passes
|
||||
- `CODEREVIEW_FINISH` - After code review passes
|
||||
|
||||
If check.jsonl doesn't exist or has no reasons, output: `ALL_CHECKS_FINISH`
|
||||
|
||||
**The loop will block you from stopping until all markers are present in your output.**
|
||||
|
||||
---
|
||||
|
||||
## 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 TYPECHECK_FINISH
|
||||
- Lint: Passed LINT_FINISH
|
||||
|
||||
### Summary
|
||||
|
||||
Checked X files, found Y issues, all fixed.
|
||||
ALL_CHECKS_FINISH
|
||||
```
|
||||
129
.opencode/agents/debug.md
Normal file
129
.opencode/agents/debug.md
Normal file
@ -0,0 +1,129 @@
|
||||
---
|
||||
description: |
|
||||
Issue fixing expert. Understands issues, fixes against specs, and verifies fixes. Precise fixes only.
|
||||
mode: subagent
|
||||
permission:
|
||||
read: allow
|
||||
write: allow
|
||||
edit: allow
|
||||
bash: allow
|
||||
glob: allow
|
||||
grep: allow
|
||||
mcp__exa__*: allow
|
||||
---
|
||||
# Debug Agent
|
||||
|
||||
You are the Debug Agent in the Trellis workflow.
|
||||
|
||||
## Context Self-Loading
|
||||
|
||||
**If you see "# Debug Agent Task" header with pre-loaded context above, skip this section.**
|
||||
|
||||
Otherwise, load context yourself:
|
||||
|
||||
1. Read `.trellis/.current-task` → get task directory (e.g., `.trellis/tasks/xxx`)
|
||||
2. Read `{task_dir}/debug.jsonl` (or `spec.jsonl` as fallback)
|
||||
3. For each entry in JSONL:
|
||||
- If `path` is a file → Read it
|
||||
- If `path` is a directory → Read all `.md` files in it
|
||||
4. Read `{task_dir}/codex-review-output.txt` if exists (Codex review results)
|
||||
|
||||
Then proceed with the workflow below using the loaded context.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Before debugging, read:
|
||||
- `.trellis/spec/` - Development guidelines
|
||||
- Error messages or issue descriptions provided
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
1. **Understand issues** - Analyze error messages or reported issues
|
||||
2. **Fix against specs** - Fix issues following dev specs
|
||||
3. **Verify fixes** - Run typecheck to ensure no new issues
|
||||
4. **Report results** - Report fix status
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Understand Issues
|
||||
|
||||
Parse the issue, categorize by priority:
|
||||
|
||||
- `[P1]` - Must fix (blocking)
|
||||
- `[P2]` - Should fix (important)
|
||||
- `[P3]` - Optional fix (nice to have)
|
||||
|
||||
### Step 2: Research if Needed
|
||||
|
||||
If you need additional info:
|
||||
|
||||
```bash
|
||||
# Check knowledge base
|
||||
ls .trellis/big-question/
|
||||
```
|
||||
|
||||
### Step 3: Fix One by One
|
||||
|
||||
For each issue:
|
||||
|
||||
1. Locate the exact position
|
||||
2. Fix following specs
|
||||
3. Run typecheck to verify
|
||||
|
||||
### Step 4: Verify
|
||||
|
||||
Run project's lint and typecheck commands to verify fixes.
|
||||
|
||||
If fix introduces new issues:
|
||||
|
||||
1. Revert the fix
|
||||
2. Use a more complete solution
|
||||
3. Re-verify
|
||||
|
||||
---
|
||||
|
||||
## Report Format
|
||||
|
||||
```markdown
|
||||
## Fix Report
|
||||
|
||||
### Issues Fixed
|
||||
|
||||
1. `[P1]` `<file>:<line>` - <what was fixed>
|
||||
2. `[P2]` `<file>:<line>` - <what was fixed>
|
||||
|
||||
### Issues Not Fixed
|
||||
|
||||
- `<file>:<line>` - <reason why not fixed>
|
||||
|
||||
### Verification
|
||||
|
||||
- TypeCheck: Pass
|
||||
- Lint: Pass
|
||||
|
||||
### Summary
|
||||
|
||||
Fixed X/Y issues. Z issues require discussion.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Guidelines
|
||||
|
||||
### DO
|
||||
|
||||
- Precise fixes for reported issues
|
||||
- Follow specs
|
||||
- Verify each fix
|
||||
|
||||
### DON'T
|
||||
|
||||
- Don't refactor surrounding code
|
||||
- Don't add new features
|
||||
- Don't modify unrelated files
|
||||
- Don't use non-null assertion (`x!` operator)
|
||||
- Don't execute git commit
|
||||
223
.opencode/agents/dispatch.md
Normal file
223
.opencode/agents/dispatch.md
Normal file
@ -0,0 +1,223 @@
|
||||
---
|
||||
description: |
|
||||
Multi-Agent Pipeline main dispatcher. Pure dispatcher. Only responsible for calling subagents and scripts in phase order.
|
||||
mode: primary
|
||||
permission:
|
||||
read: allow
|
||||
write: deny
|
||||
edit: deny
|
||||
bash: allow
|
||||
glob: deny
|
||||
grep: deny
|
||||
task: allow
|
||||
mcp__exa__*: allow
|
||||
---
|
||||
# Dispatch Agent
|
||||
|
||||
You are the Dispatch Agent in the Multi-Agent Pipeline (pure dispatcher).
|
||||
|
||||
## Working Directory Convention
|
||||
|
||||
Current Task is specified by `.trellis/.current-task` file, content is the relative path to task directory.
|
||||
|
||||
Task directory path format: `.trellis/tasks/{MM}-{DD}-{name}/`
|
||||
|
||||
This directory contains all context files for the current task:
|
||||
|
||||
- `task.json` - Task configuration
|
||||
- `prd.md` - Requirements document
|
||||
- `info.md` - Technical design (optional)
|
||||
- `implement.jsonl` - Implement context
|
||||
- `check.jsonl` - Check context
|
||||
- `debug.jsonl` - Debug context
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **You are a pure dispatcher** - Only responsible for calling subagents and scripts in order
|
||||
2. **You don't read specs/requirements** - Hook will auto-inject all context to subagents
|
||||
3. **You don't need resume** - Hook injects complete context on each subagent call
|
||||
4. **You only need simple commands** - Tell subagent "start working" is enough
|
||||
|
||||
---
|
||||
|
||||
## Startup Flow
|
||||
|
||||
### Step 1: Determine Current Task Directory
|
||||
|
||||
Read `.trellis/.current-task` to get current task directory path:
|
||||
|
||||
```bash
|
||||
TASK_DIR=$(cat .trellis/.current-task)
|
||||
# e.g.: .trellis/tasks/02-03-my-feature
|
||||
```
|
||||
|
||||
### Step 2: Read Task Configuration
|
||||
|
||||
```bash
|
||||
cat ${TASK_DIR}/task.json
|
||||
```
|
||||
|
||||
Get the `next_action` array, which defines the list of phases to execute.
|
||||
|
||||
### Step 3: Execute in Phase Order
|
||||
|
||||
Execute each step in `phase` order.
|
||||
|
||||
> **Note**: You do NOT need to manually update `current_phase`. The Hook automatically updates it when you call Task with a subagent.
|
||||
|
||||
---
|
||||
|
||||
## Phase Handling
|
||||
|
||||
> Hook will auto-inject all specs, requirements, and technical design to subagent context.
|
||||
> Dispatch only needs to issue simple call commands.
|
||||
>
|
||||
> **OpenCode dispatch rule**: Call subagents synchronously (`run_in_background: false`).
|
||||
> Do NOT use `TaskOutput` or background polling as the completion signal for child phases.
|
||||
> The background wrapper can finish before the real subagent session is actually done.
|
||||
|
||||
### action: "implement"
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "implement",
|
||||
prompt: "Implement the feature described in prd.md in the task directory",
|
||||
model: "opus",
|
||||
run_in_background: false
|
||||
)
|
||||
```
|
||||
|
||||
Hook will auto-inject:
|
||||
|
||||
- All spec files from implement.jsonl
|
||||
- Requirements document (prd.md)
|
||||
- Technical design (info.md)
|
||||
|
||||
Implement receives complete context and autonomously: read → understand → implement.
|
||||
|
||||
### action: "check"
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "check",
|
||||
prompt: "Check code changes, fix issues yourself",
|
||||
model: "opus",
|
||||
run_in_background: false
|
||||
)
|
||||
```
|
||||
|
||||
Hook will auto-inject:
|
||||
|
||||
- finish-work.md
|
||||
- check-cross-layer.md
|
||||
- check.md
|
||||
- All spec files from check.jsonl
|
||||
|
||||
### action: "debug"
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "debug",
|
||||
prompt: "Fix the issues described in the task context",
|
||||
model: "opus",
|
||||
run_in_background: false
|
||||
)
|
||||
```
|
||||
|
||||
Hook will auto-inject:
|
||||
|
||||
- All spec files from debug.jsonl
|
||||
- Error context if available
|
||||
|
||||
### action: "finish"
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "check",
|
||||
prompt: "[finish] Execute final completion check before PR",
|
||||
model: "opus",
|
||||
run_in_background: false
|
||||
)
|
||||
```
|
||||
|
||||
**Important**: The `[finish]` marker in prompt triggers different context injection:
|
||||
- finish-work.md checklist
|
||||
- update-spec.md (spec update process and templates)
|
||||
- prd.md for verifying requirements are met
|
||||
|
||||
The finish agent actively updates spec docs when it detects new patterns or contracts in the changes.
|
||||
|
||||
This is different from regular "check" which has full specs for self-fix loop.
|
||||
|
||||
### action: "create-pr"
|
||||
|
||||
This action creates a Pull Request from the feature branch. Run it via Bash:
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/multi_agent/create_pr.py
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Stage and commit all changes (excluding workspace)
|
||||
2. Push to origin
|
||||
3. Create a Draft PR using `gh pr create`
|
||||
4. Update task.json with status="review", pr_url, and current_phase
|
||||
|
||||
**Note**: This is the only action that performs git commit, as it's the final step after all implementation and checks are complete.
|
||||
|
||||
---
|
||||
|
||||
## Calling Subagents
|
||||
|
||||
### Basic Pattern
|
||||
|
||||
```
|
||||
result = Task(
|
||||
subagent_type: "implement", // or "check", "debug"
|
||||
prompt: "Simple task description",
|
||||
model: "opus",
|
||||
run_in_background: false
|
||||
)
|
||||
|
||||
// Wait for the Task call to return before starting the next phase.
|
||||
// Do NOT call TaskOutput or use background polling inside OpenCode dispatch.
|
||||
```
|
||||
|
||||
### Execution Rule
|
||||
|
||||
- Run one phase at a time
|
||||
- Start the next phase only after the current `Task(...)` call returns
|
||||
- If a phase returns a clear timeout or failure, handle that result explicitly
|
||||
- Do **not** simulate completion by polling a background task wrapper
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Timeout
|
||||
|
||||
If a synchronous subagent call times out, notify the user and ask for guidance:
|
||||
|
||||
```
|
||||
"Subagent {phase} timed out after {time}. Options:
|
||||
1. Retry the same phase
|
||||
2. Skip to next phase
|
||||
3. Abort the pipeline"
|
||||
```
|
||||
|
||||
### Subagent Failure
|
||||
|
||||
If a synchronous subagent call reports failure, read the output and decide:
|
||||
|
||||
- If recoverable: call debug agent to fix
|
||||
- If not recoverable: notify user and ask for guidance
|
||||
- Do not switch back to `TaskOutput` polling for the same phase
|
||||
|
||||
---
|
||||
|
||||
## Key Constraints
|
||||
|
||||
1. **Do not read spec/requirement files directly** - Let Hook inject to subagents
|
||||
2. **Only commit via create-pr action** - Use `multi_agent/create_pr.py` at the end of pipeline
|
||||
3. **All subagents should use opus model for complex tasks**
|
||||
4. **Keep dispatch logic simple** - Complex logic belongs in subagents
|
||||
120
.opencode/agents/implement.md
Normal file
120
.opencode/agents/implement.md
Normal file
@ -0,0 +1,120 @@
|
||||
---
|
||||
description: |
|
||||
Code implementation expert. Understands specs and requirements, then implements features. No git commit allowed.
|
||||
mode: subagent
|
||||
permission:
|
||||
read: allow
|
||||
write: allow
|
||||
edit: allow
|
||||
bash: allow
|
||||
glob: allow
|
||||
grep: allow
|
||||
mcp__exa__*: allow
|
||||
---
|
||||
# Implement Agent
|
||||
|
||||
You are the Implement Agent in the Trellis workflow.
|
||||
|
||||
## Context Self-Loading
|
||||
|
||||
**If you see "# Implement Agent Task" header with pre-loaded context above, skip this section.**
|
||||
|
||||
Otherwise, load context yourself:
|
||||
|
||||
1. Read `.trellis/.current-task` → get task directory (e.g., `.trellis/tasks/xxx`)
|
||||
2. Read `{task_dir}/implement.jsonl` (or `spec.jsonl` as fallback)
|
||||
3. For each entry in JSONL:
|
||||
- If `path` is a file → Read it
|
||||
- If `path` is a directory → Read all `.md` files in it
|
||||
4. Read `{task_dir}/prd.md` for requirements
|
||||
5. Read `{task_dir}/info.md` for technical design (if exists)
|
||||
|
||||
Then proceed with the workflow below using the loaded context.
|
||||
|
||||
---
|
||||
|
||||
## 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/`
|
||||
- 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
|
||||
146
.opencode/agents/research.md
Normal file
146
.opencode/agents/research.md
Normal file
@ -0,0 +1,146 @@
|
||||
---
|
||||
description: |
|
||||
Code and tech search expert. Pure research, no code modifications. Finds files, patterns, and tech solutions.
|
||||
mode: subagent
|
||||
permission:
|
||||
read: allow
|
||||
write: deny
|
||||
edit: deny
|
||||
bash: deny
|
||||
glob: allow
|
||||
grep: allow
|
||||
mcp__exa__*: allow
|
||||
mcp__chrome-devtools__*: allow
|
||||
---
|
||||
# Research Agent
|
||||
|
||||
You are the Research Agent in the Trellis workflow.
|
||||
|
||||
## Context Self-Loading
|
||||
|
||||
**If you see "# Research Agent Task" header with pre-loaded context above, skip this section.**
|
||||
|
||||
Otherwise, if task-specific research is needed:
|
||||
|
||||
1. Read `.trellis/.current-task` → get task directory (if exists)
|
||||
2. Read `{task_dir}/research.jsonl` if exists
|
||||
3. For each entry in JSONL:
|
||||
- If `path` is a file → Read it
|
||||
- If `path` is a directory → Read all `.md` files in it
|
||||
|
||||
Project spec locations for reference:
|
||||
- `.trellis/spec/<package>/<layer>/` - Package-specific standards
|
||||
- `.trellis/spec/guides/` - Thinking guides
|
||||
- `.trellis/big-question/` - Known issues and pitfalls
|
||||
|
||||
---
|
||||
|
||||
## Core Principle
|
||||
|
||||
**You do one thing: find and explain information.**
|
||||
|
||||
You are a documenter, not a reviewer. Your job is to help get the information needed.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
### 1. Internal Search (Project Code)
|
||||
|
||||
| Search Type | Goal | Tools |
|
||||
|-------------|------|-------|
|
||||
| **WHERE** | Locate files/components | Glob, Grep |
|
||||
| **HOW** | Understand code logic | Read, Grep |
|
||||
| **PATTERN** | Discover existing patterns | Grep, Read |
|
||||
|
||||
### 2. External Search (Tech Solutions)
|
||||
|
||||
Use web search for best practices and code examples.
|
||||
|
||||
---
|
||||
|
||||
## Strict Boundaries
|
||||
|
||||
### Only Allowed
|
||||
|
||||
- Describe **what exists**
|
||||
- Describe **where it is**
|
||||
- Describe **how it works**
|
||||
- Describe **how components interact**
|
||||
|
||||
### Forbidden (unless explicitly asked)
|
||||
|
||||
- Suggest improvements
|
||||
- Criticize implementation
|
||||
- Recommend refactoring
|
||||
- Modify any files
|
||||
- Execute git commands
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Understand Search Request
|
||||
|
||||
Analyze the query, determine:
|
||||
|
||||
- Search type (internal/external/mixed)
|
||||
- Search scope (global/specific directory)
|
||||
- Expected output (file list/code patterns/tech solutions)
|
||||
|
||||
### Step 2: Execute Search
|
||||
|
||||
Execute multiple independent searches in parallel for efficiency.
|
||||
|
||||
### Step 3: Organize Results
|
||||
|
||||
Output structured results in report format.
|
||||
|
||||
---
|
||||
|
||||
## Report Format
|
||||
|
||||
```markdown
|
||||
## Search Results
|
||||
|
||||
### Query
|
||||
|
||||
{original query}
|
||||
|
||||
### Files Found
|
||||
|
||||
| File Path | Description |
|
||||
|-----------|-------------|
|
||||
| `src/services/xxx.ts` | Main implementation |
|
||||
| `src/types/xxx.ts` | Type definitions |
|
||||
|
||||
### Code Pattern Analysis
|
||||
|
||||
{Describe discovered patterns, cite specific files and line numbers}
|
||||
|
||||
### Related Spec Documents
|
||||
|
||||
- `.trellis/spec/xxx.md` - {description}
|
||||
|
||||
### Not Found
|
||||
|
||||
{If some content was not found, explain}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Guidelines
|
||||
|
||||
### DO
|
||||
|
||||
- Provide specific file paths and line numbers
|
||||
- Quote actual code snippets
|
||||
- Distinguish "definitely found" and "possibly related"
|
||||
- Explain search scope and limitations
|
||||
|
||||
### DON'T
|
||||
|
||||
- Don't guess uncertain info
|
||||
- Don't omit important search results
|
||||
- Don't add improvement suggestions in report (unless explicitly asked)
|
||||
- Don't modify any files
|
||||
427
.opencode/agents/trellis-plan.md
Normal file
427
.opencode/agents/trellis-plan.md
Normal file
@ -0,0 +1,427 @@
|
||||
---
|
||||
description: |
|
||||
Multi-Agent Pipeline planner. Analyzes requirements and produces a fully configured task directory ready for dispatch.
|
||||
mode: primary
|
||||
permission:
|
||||
read: allow
|
||||
write: allow
|
||||
edit: allow
|
||||
bash: allow
|
||||
glob: allow
|
||||
grep: allow
|
||||
task: allow
|
||||
---
|
||||
# Plan Agent
|
||||
|
||||
You are the Plan Agent in the Multi-Agent Pipeline.
|
||||
|
||||
**Your job**: Evaluate requirements and, if valid, transform them into a fully configured task directory.
|
||||
|
||||
**You have the power to reject** - If a requirement is unclear, incomplete, unreasonable, or potentially harmful, you MUST refuse to proceed and clean up.
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL: You MUST Execute Tools
|
||||
|
||||
**DO NOT just output text descriptions of what you would do.**
|
||||
**You MUST actually execute bash commands and use tools to perform actions.**
|
||||
|
||||
When this prompt says "run this command", you must use the bash tool to execute it.
|
||||
When this prompt says "write this file", you must use the write tool to create it.
|
||||
|
||||
---
|
||||
|
||||
## Step 0: Read Environment Variables (REQUIRED FIRST STEP)
|
||||
|
||||
**IMMEDIATELY execute this bash command to read your input:**
|
||||
|
||||
```bash
|
||||
echo "PLAN_TASK_NAME=$PLAN_TASK_NAME"
|
||||
echo "PLAN_DEV_TYPE=$PLAN_DEV_TYPE"
|
||||
echo "PLAN_REQUIREMENT=$PLAN_REQUIREMENT"
|
||||
echo "PLAN_TASK_DIR=$PLAN_TASK_DIR"
|
||||
```
|
||||
|
||||
This gives you the task configuration. Store these values for use in subsequent steps.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Evaluate Requirement (CRITICAL)
|
||||
|
||||
Now evaluate the requirement from `$PLAN_REQUIREMENT`:
|
||||
|
||||
### Reject If:
|
||||
|
||||
1. **Unclear or Vague**
|
||||
- "Make it better" / "Fix the bugs" / "Improve performance"
|
||||
- No specific outcome defined
|
||||
- Cannot determine what "done" looks like
|
||||
|
||||
2. **Incomplete Information**
|
||||
- Missing critical details to implement
|
||||
- References unknown systems or files
|
||||
- Depends on decisions not yet made
|
||||
|
||||
3. **Out of Scope for This Project**
|
||||
- Requirement doesn't match the project's purpose
|
||||
- Requires changes to external systems
|
||||
- Not technically feasible with current architecture
|
||||
|
||||
4. **Potentially Harmful**
|
||||
- Security vulnerabilities (intentional backdoors, data exfiltration)
|
||||
- Destructive operations without clear justification
|
||||
- Circumventing access controls
|
||||
|
||||
5. **Too Large / Should Be Split**
|
||||
- Multiple unrelated features bundled together
|
||||
- Would require touching too many systems
|
||||
- Cannot be completed in a reasonable scope
|
||||
|
||||
### If Rejecting:
|
||||
|
||||
**You MUST execute these commands using the bash tool. Do not just describe them.**
|
||||
|
||||
**Step R1: Update task.json status** - Execute this bash command:
|
||||
```bash
|
||||
jq '.status = "rejected"' "$PLAN_TASK_DIR/task.json" > "$PLAN_TASK_DIR/task.json.tmp" \
|
||||
&& mv "$PLAN_TASK_DIR/task.json.tmp" "$PLAN_TASK_DIR/task.json"
|
||||
```
|
||||
|
||||
**Step R2: Write REJECTED.md** - Use the write tool to create `$PLAN_TASK_DIR/REJECTED.md` with this content:
|
||||
```markdown
|
||||
# Plan Rejected
|
||||
|
||||
## Reason
|
||||
<category from above>
|
||||
|
||||
## Details
|
||||
<specific explanation of why this requirement cannot proceed>
|
||||
|
||||
## Suggestions
|
||||
- <what the user should clarify or change>
|
||||
- <how to make the requirement actionable>
|
||||
|
||||
## To Retry
|
||||
|
||||
1. Delete this directory:
|
||||
```bash
|
||||
rm -rf <task_dir>
|
||||
```
|
||||
|
||||
2. Run with revised requirement:
|
||||
```bash
|
||||
python3 ./.trellis/scripts/multi_agent/plan.py --name "<name>" --type "<type>" --requirement "<revised requirement>"
|
||||
```
|
||||
```
|
||||
|
||||
**Step R3: Print summary** - Execute:
|
||||
```bash
|
||||
echo "=== PLAN REJECTED ==="
|
||||
echo ""
|
||||
echo "Reason: <category>"
|
||||
echo "Details: <brief explanation>"
|
||||
echo ""
|
||||
echo "See: $PLAN_TASK_DIR/REJECTED.md"
|
||||
```
|
||||
|
||||
**Step R4: Stop** - Do not proceed to acceptance workflow.
|
||||
|
||||
**The task directory is kept** with:
|
||||
- `task.json` (status: "rejected")
|
||||
- `REJECTED.md` (full explanation)
|
||||
- `.plan-log` (execution log)
|
||||
|
||||
This allows the user to review why it was rejected.
|
||||
|
||||
### If Accepting:
|
||||
|
||||
Continue to Step 1. The requirement is:
|
||||
- Clear and specific
|
||||
- Has a defined outcome
|
||||
- Is technically feasible
|
||||
- Is appropriately scoped
|
||||
|
||||
---
|
||||
|
||||
## Input
|
||||
|
||||
You receive input via environment variables (set by plan.py):
|
||||
|
||||
```bash
|
||||
PLAN_TASK_NAME # Task name (e.g., "user-auth")
|
||||
PLAN_DEV_TYPE # Development type: backend | frontend | fullstack
|
||||
PLAN_REQUIREMENT # Requirement description from user
|
||||
PLAN_TASK_DIR # Pre-created task directory path
|
||||
```
|
||||
|
||||
Read them at startup:
|
||||
|
||||
```bash
|
||||
echo "Task: $PLAN_TASK_NAME"
|
||||
echo "Type: $PLAN_DEV_TYPE"
|
||||
echo "Requirement: $PLAN_REQUIREMENT"
|
||||
echo "Directory: $PLAN_TASK_DIR"
|
||||
```
|
||||
|
||||
## Output (if accepted)
|
||||
|
||||
A complete task directory containing:
|
||||
|
||||
```
|
||||
${PLAN_TASK_DIR}/
|
||||
├── task.json # Updated with branch, scope, dev_type
|
||||
├── prd.md # Requirements document
|
||||
├── implement.jsonl # Implement phase context
|
||||
├── check.jsonl # Check phase context
|
||||
└── debug.jsonl # Debug phase context
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow (After Acceptance)
|
||||
|
||||
### Step 1: Initialize Context Files
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py init-context "$PLAN_TASK_DIR" "$PLAN_DEV_TYPE"
|
||||
```
|
||||
|
||||
This creates base jsonl files with standard specs for the dev type.
|
||||
|
||||
### Step 2: Analyze Codebase with Research Agent
|
||||
|
||||
Call research agent to find relevant specs and code patterns:
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "research",
|
||||
prompt: "Analyze what specs and code patterns are needed for this task.
|
||||
|
||||
Task: ${PLAN_REQUIREMENT}
|
||||
Dev Type: ${PLAN_DEV_TYPE}
|
||||
|
||||
Instructions:
|
||||
1. Search .trellis/spec/ for relevant spec files
|
||||
2. Search the codebase for related modules and patterns
|
||||
3. Identify files that should be added to jsonl context
|
||||
|
||||
Output format (use exactly this format):
|
||||
|
||||
## implement.jsonl
|
||||
- path: <relative file path>, reason: <why needed>
|
||||
- path: <relative file path>, reason: <why needed>
|
||||
|
||||
## check.jsonl
|
||||
- path: <relative file path>, reason: <why needed>
|
||||
|
||||
## debug.jsonl
|
||||
- path: <relative file path>, reason: <why needed>
|
||||
|
||||
## Suggested Scope
|
||||
<single word for commit scope, e.g., auth, api, ui>
|
||||
|
||||
## Technical Notes
|
||||
<any important technical considerations for prd.md>",
|
||||
model: "opus"
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3: Add Context Entries
|
||||
|
||||
Parse research agent output and add entries to jsonl files:
|
||||
|
||||
```bash
|
||||
# For each entry in implement.jsonl section:
|
||||
python3 ./.trellis/scripts/task.py add-context "$PLAN_TASK_DIR" implement "<path>" "<reason>"
|
||||
|
||||
# For each entry in check.jsonl section:
|
||||
python3 ./.trellis/scripts/task.py add-context "$PLAN_TASK_DIR" check "<path>" "<reason>"
|
||||
|
||||
# For each entry in debug.jsonl section:
|
||||
python3 ./.trellis/scripts/task.py add-context "$PLAN_TASK_DIR" debug "<path>" "<reason>"
|
||||
```
|
||||
|
||||
### Step 4: Write prd.md
|
||||
|
||||
Create the requirements document:
|
||||
|
||||
```bash
|
||||
cat > "$PLAN_TASK_DIR/prd.md" << 'EOF'
|
||||
# Task: ${PLAN_TASK_NAME}
|
||||
|
||||
## Overview
|
||||
[Brief description of what this feature does]
|
||||
|
||||
## Requirements
|
||||
- [Requirement 1]
|
||||
- [Requirement 2]
|
||||
- ...
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] [Criterion 1]
|
||||
- [ ] [Criterion 2]
|
||||
- ...
|
||||
|
||||
## Technical Notes
|
||||
[Any technical considerations from research agent]
|
||||
|
||||
## Out of Scope
|
||||
- [What this feature does NOT include]
|
||||
EOF
|
||||
```
|
||||
|
||||
**Guidelines for prd.md**:
|
||||
- Be specific and actionable
|
||||
- Include acceptance criteria that can be verified
|
||||
- Add technical notes from research agent
|
||||
- Define what's out of scope to prevent scope creep
|
||||
|
||||
### Step 5: Configure Task Metadata
|
||||
|
||||
```bash
|
||||
# Set branch name
|
||||
python3 ./.trellis/scripts/task.py set-branch "$PLAN_TASK_DIR" "feature/${PLAN_TASK_NAME}"
|
||||
|
||||
# Set scope (from research agent suggestion)
|
||||
python3 ./.trellis/scripts/task.py set-scope "$PLAN_TASK_DIR" "<scope>"
|
||||
|
||||
# Update dev_type in task.json
|
||||
jq --arg type "$PLAN_DEV_TYPE" '.dev_type = $type' \
|
||||
"$PLAN_TASK_DIR/task.json" > "$PLAN_TASK_DIR/task.json.tmp" \
|
||||
&& mv "$PLAN_TASK_DIR/task.json.tmp" "$PLAN_TASK_DIR/task.json"
|
||||
```
|
||||
|
||||
### Step 6: Validate Configuration
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py validate "$PLAN_TASK_DIR"
|
||||
```
|
||||
|
||||
If validation fails, fix the invalid paths and re-validate.
|
||||
|
||||
### Step 7: Output Summary
|
||||
|
||||
Print a summary for the caller:
|
||||
|
||||
```bash
|
||||
echo "=== Plan Complete ==="
|
||||
echo "Task Directory: $PLAN_TASK_DIR"
|
||||
echo ""
|
||||
echo "Files created:"
|
||||
ls -la "$PLAN_TASK_DIR"
|
||||
echo ""
|
||||
echo "Context summary:"
|
||||
python3 ./.trellis/scripts/task.py list-context "$PLAN_TASK_DIR"
|
||||
echo ""
|
||||
echo "Ready for: python3 ./.trellis/scripts/multi_agent/start.py $PLAN_TASK_DIR"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Principles
|
||||
|
||||
1. **Reject early, reject clearly** - Don't waste time on bad requirements
|
||||
2. **Research before configure** - Always call research agent to understand the codebase
|
||||
3. **Validate all paths** - Every file in jsonl must exist
|
||||
4. **Be specific in prd.md** - Vague requirements lead to wrong implementations
|
||||
5. **Include acceptance criteria** - Check agent needs to verify something concrete
|
||||
6. **Set appropriate scope** - This affects commit message format
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Research Agent Returns No Results
|
||||
|
||||
If research agent finds no relevant specs:
|
||||
- Use only the base specs from init-context
|
||||
- Add a note in prd.md that this is a new area without existing patterns
|
||||
|
||||
### Path Not Found
|
||||
|
||||
If add-context fails because path doesn't exist:
|
||||
- Skip that entry
|
||||
- Log a warning
|
||||
- Continue with other entries
|
||||
|
||||
### Validation Fails
|
||||
|
||||
If final validation fails:
|
||||
- Read the error output
|
||||
- Remove invalid entries from jsonl files
|
||||
- Re-validate
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Example: Accepted Requirement
|
||||
|
||||
```
|
||||
Input:
|
||||
PLAN_TASK_NAME = "add-rate-limiting"
|
||||
PLAN_DEV_TYPE = "backend"
|
||||
PLAN_REQUIREMENT = "Add rate limiting to API endpoints using a sliding window algorithm. Limit to 100 requests per minute per IP. Return 429 status when exceeded."
|
||||
|
||||
Result: ACCEPTED - Clear, specific, has defined behavior
|
||||
|
||||
Output:
|
||||
.trellis/tasks/02-03-add-rate-limiting/
|
||||
├── task.json # branch: feature/add-rate-limiting, scope: api
|
||||
├── prd.md # Detailed requirements with acceptance criteria
|
||||
├── implement.jsonl # Backend specs + existing middleware patterns
|
||||
├── check.jsonl # Quality guidelines + API testing specs
|
||||
└── debug.jsonl # Error handling specs
|
||||
```
|
||||
|
||||
### Example: Rejected - Vague Requirement
|
||||
|
||||
```
|
||||
Input:
|
||||
PLAN_REQUIREMENT = "Make the API faster"
|
||||
|
||||
Result: REJECTED
|
||||
|
||||
=== PLAN REJECTED ===
|
||||
|
||||
Reason: Unclear or Vague
|
||||
|
||||
Details:
|
||||
"Make the API faster" does not specify:
|
||||
- Which endpoints need optimization
|
||||
- Current performance baseline
|
||||
- Target performance metrics
|
||||
- Acceptable trade-offs (memory, complexity)
|
||||
|
||||
Suggestions:
|
||||
- Identify specific slow endpoints with response times
|
||||
- Define target latency (e.g., "GET /users should respond in <100ms")
|
||||
- Specify if caching, query optimization, or architecture changes are acceptable
|
||||
```
|
||||
|
||||
### Example: Rejected - Too Large
|
||||
|
||||
```
|
||||
Input:
|
||||
PLAN_REQUIREMENT = "Add user authentication, authorization, password reset, 2FA, OAuth integration, and audit logging"
|
||||
|
||||
Result: REJECTED
|
||||
|
||||
=== PLAN REJECTED ===
|
||||
|
||||
Reason: Too Large / Should Be Split
|
||||
|
||||
Details:
|
||||
This requirement bundles 6 distinct features that should be implemented separately:
|
||||
1. User authentication (login/logout)
|
||||
2. Authorization (roles/permissions)
|
||||
3. Password reset flow
|
||||
4. Two-factor authentication
|
||||
5. OAuth integration
|
||||
6. Audit logging
|
||||
|
||||
Suggestions:
|
||||
- Start with basic authentication first
|
||||
- Create separate features for each capability
|
||||
- Consider dependencies (auth before authz, etc.)
|
||||
```
|
||||
29
.opencode/commands/trellis/before-dev.md
Normal file
29
.opencode/commands/trellis/before-dev.md
Normal file
@ -0,0 +1,29 @@
|
||||
Read the relevant development guidelines before starting your task.
|
||||
|
||||
Execute these steps:
|
||||
|
||||
1. **Discover packages and their spec layers**:
|
||||
```bash
|
||||
python3 ./.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.
|
||||
487
.opencode/commands/trellis/brainstorm.md
Normal file
487
.opencode/commands/trellis/brainstorm.md
Normal file
@ -0,0 +1,487 @@
|
||||
# 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 2–3 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=$(python3 ./.trellis/scripts/task.py create "brainstorm: <short goal>" --slug <auto>)
|
||||
```
|
||||
|
||||
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, 1–2 files, scope well-defined | Ask 1 confirm question, then implement |
|
||||
| **Moderate** | Multiple files, some ambiguity | Light brainstorm (2–3 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
|
||||
|
||||
### Research steps
|
||||
|
||||
1. Identify 2–4 comparable tools/patterns
|
||||
2. Summarize common conventions and why they exist
|
||||
3. Map conventions onto our repo constraints
|
||||
4. Produce **2–3 feasible approaches** for our project
|
||||
|
||||
### Research output format (PRD)
|
||||
|
||||
Add a section in PRD (either within Technical Notes or as its own):
|
||||
|
||||
```markdown
|
||||
## 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 1–2 bullets each)
|
||||
|
||||
1. **Future evolution**
|
||||
|
||||
* What might this feature become in 1–3 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: <1–2 bullets>
|
||||
2. Related scenarios: <1–2 bullets>
|
||||
3. Failure/edge cases: <1–2 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 2–3 approaches (if not already done via research-first):
|
||||
|
||||
```markdown
|
||||
Based on current information, here are 2–3 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=$(python3 ./.trellis/scripts/task.py create "Child task 1" --slug child1 --parent "$TASK_DIR")
|
||||
CHILD2=$(python3 ./.trellis/scripts/task.py create "Child task 2" --slug child2 --parent "$TASK_DIR")
|
||||
|
||||
# Or link existing tasks
|
||||
python3 ./.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 1–7: 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 |
|
||||
125
.opencode/commands/trellis/break-loop.md
Normal file
125
.opencode/commands/trellis/break-loop.md
Normal file
@ -0,0 +1,125 @@
|
||||
# Break the Loop - Deep Bug Analysis
|
||||
|
||||
When debug is complete, use this command 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** | TypeScript strict, no any | 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 `.trellis/spec/backend/` or `frontend/` docs
|
||||
- [ ] Create issue record (if applicable)
|
||||
- [ ] Create feature ticket for root fix
|
||||
- [ ] Update check commands 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.**
|
||||
153
.opencode/commands/trellis/check-cross-layer.md
Normal file
153
.opencode/commands/trellis/check-cross-layer.md
Normal file
@ -0,0 +1,153 @@
|
||||
# Cross-Layer Check
|
||||
|
||||
Check if your changes considered all dimensions. Most bugs come from "didn't think of it", not lack of technical skill.
|
||||
|
||||
> **Note**: This is a **post-implementation** safety net. Ideally, read the [Pre-Implementation Checklist](.trellis/spec/guides/pre-implementation-checklist.md) **before** writing code.
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
| Document | Purpose | Timing |
|
||||
|----------|---------|--------|
|
||||
| [Pre-Implementation Checklist](.trellis/spec/guides/pre-implementation-checklist.md) | Questions before coding | **Before** writing code |
|
||||
| [Code Reuse Thinking Guide](.trellis/spec/guides/code-reuse-thinking-guide.md) | Pattern recognition | During implementation |
|
||||
| **`/trellis:check-cross-layer`** (this) | Verification check | **After** implementation |
|
||||
|
||||
---
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Identify Change Scope
|
||||
|
||||
```bash
|
||||
git status
|
||||
git diff --name-only
|
||||
```
|
||||
|
||||
### 2. Select Applicable Check Dimensions
|
||||
|
||||
Based on your change type, execute relevant checks below:
|
||||
|
||||
---
|
||||
|
||||
## Dimension A: Cross-Layer Data Flow (Required when 3+ layers)
|
||||
|
||||
**Trigger**: Changes involve 3 or more layers
|
||||
|
||||
| Layer | Common Locations |
|
||||
|-------|------------------|
|
||||
| API/Routes | `routes/`, `api/`, `handlers/`, `controllers/` |
|
||||
| Service/Business Logic | `services/`, `lib/`, `core/`, `domain/` |
|
||||
| Database/Storage | `db/`, `models/`, `repositories/`, `schema/` |
|
||||
| UI/Presentation | `components/`, `views/`, `templates/`, `pages/` |
|
||||
| Utility | `utils/`, `helpers/`, `common/` |
|
||||
|
||||
**Checklist**:
|
||||
- [ ] Read flow: Database -> Service -> API -> UI
|
||||
- [ ] Write flow: UI -> API -> Service -> Database
|
||||
- [ ] Types/schemas correctly passed between layers?
|
||||
- [ ] Errors properly propagated to caller?
|
||||
- [ ] Loading/pending states handled at each layer?
|
||||
|
||||
**Detailed Guide**: `.trellis/spec/guides/cross-layer-thinking-guide.md`
|
||||
|
||||
---
|
||||
|
||||
## Dimension B: Code Reuse (Required when modifying constants/config)
|
||||
|
||||
**Trigger**:
|
||||
- Modifying UI constants (label, icon, color)
|
||||
- Modifying any hardcoded value
|
||||
- Seeing similar code in multiple places
|
||||
- Creating a new utility/helper function
|
||||
- Just finished batch modifications across files
|
||||
|
||||
**Checklist**:
|
||||
- [ ] Search first: How many places define this value?
|
||||
```bash
|
||||
# Search in source files (adjust extensions for your project)
|
||||
grep -r "value-to-change" src/
|
||||
```
|
||||
- [ ] If 2+ places define same value -> Should extract to shared constant
|
||||
- [ ] After modification, all usage sites updated?
|
||||
- [ ] If creating utility: Does similar utility already exist?
|
||||
|
||||
**Detailed Guide**: `.trellis/spec/guides/code-reuse-thinking-guide.md`
|
||||
|
||||
---
|
||||
|
||||
## Dimension B2: New Utility Functions
|
||||
|
||||
**Trigger**: About to create a new utility/helper function
|
||||
|
||||
**Checklist**:
|
||||
- [ ] Search for existing similar utilities first
|
||||
```bash
|
||||
grep -r "functionNamePattern" src/
|
||||
```
|
||||
- [ ] If similar exists, can you extend it instead?
|
||||
- [ ] If creating new, is it in the right location (shared vs domain-specific)?
|
||||
|
||||
---
|
||||
|
||||
## Dimension B3: After Batch Modifications
|
||||
|
||||
**Trigger**: Just modified similar patterns in multiple files
|
||||
|
||||
**Checklist**:
|
||||
- [ ] Did you check ALL files with similar patterns?
|
||||
```bash
|
||||
grep -r "patternYouChanged" src/
|
||||
```
|
||||
- [ ] Any files missed that should also be updated?
|
||||
- [ ] Should this pattern be abstracted to prevent future duplication?
|
||||
|
||||
---
|
||||
|
||||
## Dimension C: Import/Dependency Paths (Required when creating new files)
|
||||
|
||||
**Trigger**: Creating new source files
|
||||
|
||||
**Checklist**:
|
||||
- [ ] Using correct import paths (relative vs absolute)?
|
||||
- [ ] No circular dependencies?
|
||||
- [ ] Consistent with project's module organization?
|
||||
|
||||
---
|
||||
|
||||
## Dimension D: Same-Layer Consistency
|
||||
|
||||
**Trigger**:
|
||||
- Modifying display logic or formatting
|
||||
- Same domain concept used in multiple places
|
||||
|
||||
**Checklist**:
|
||||
- [ ] Search for other places using same concept
|
||||
```bash
|
||||
grep -r "ConceptName" src/
|
||||
```
|
||||
- [ ] Are these usages consistent?
|
||||
- [ ] Should they share configuration/constants?
|
||||
|
||||
---
|
||||
|
||||
## Common Issues Quick Reference
|
||||
|
||||
| Issue | Root Cause | Prevention |
|
||||
|-------|------------|------------|
|
||||
| Changed one place, missed others | Didn't search impact scope | `grep` before changing |
|
||||
| Data lost at some layer | Didn't check data flow | Trace data source to destination |
|
||||
| Type/schema mismatch | Cross-layer types inconsistent | Use shared type definitions |
|
||||
| UI/output inconsistent | Same concept in multiple places | Extract shared constants |
|
||||
| Similar utility exists | Didn't search first | Search before creating |
|
||||
| Batch fix incomplete | Didn't verify all occurrences | grep after fixing |
|
||||
|
||||
---
|
||||
|
||||
## Output
|
||||
|
||||
Report:
|
||||
1. Which dimensions your changes involve
|
||||
2. Check results for each dimension
|
||||
3. Issues found and fix suggestions
|
||||
25
.opencode/commands/trellis/check.md
Normal file
25
.opencode/commands/trellis/check.md
Normal file
@ -0,0 +1,25 @@
|
||||
Check if the code you just wrote follows the development guidelines.
|
||||
|
||||
Execute these steps:
|
||||
|
||||
1. **Identify changed files**:
|
||||
```bash
|
||||
git diff --name-only HEAD
|
||||
```
|
||||
|
||||
2. **Determine which spec modules apply** based on the changed file paths:
|
||||
```bash
|
||||
python3 ./.trellis/scripts/get_context.py --mode packages
|
||||
```
|
||||
|
||||
3. **Read the spec index** for each relevant module:
|
||||
```bash
|
||||
cat .trellis/spec/<package>/<layer>/index.md
|
||||
```
|
||||
Follow the **"Quality Check"** section in the index.
|
||||
|
||||
4. **Read the specific guideline files** referenced in the Quality Check section (e.g., `quality-guidelines.md`, `conventions.md`). The index is NOT the goal — it points you to the actual guideline files. Read those files and review your code against them.
|
||||
|
||||
5. **Run lint and typecheck** for the affected package.
|
||||
|
||||
6. **Report any violations** and fix them if found.
|
||||
154
.opencode/commands/trellis/create-command.md
Normal file
154
.opencode/commands/trellis/create-command.md
Normal file
@ -0,0 +1,154 @@
|
||||
# Create New Slash Command
|
||||
|
||||
Create a new slash command in both `.cursor/commands/` (with `trellis-` prefix) and `.opencode/commands/trellis/` directories based on user requirements.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/trellis:create-command <command-name> <description>
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```
|
||||
/trellis:create-command review-pr Check PR code changes against project guidelines
|
||||
```
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Parse Input
|
||||
|
||||
Extract from user input:
|
||||
- **Command name**: Use kebab-case (e.g., `review-pr`)
|
||||
- **Description**: What the command should accomplish
|
||||
|
||||
### 2. Analyze Requirements
|
||||
|
||||
Determine command type based on description:
|
||||
- **Initialization**: Read docs, establish context
|
||||
- **Pre-development**: Read guidelines, check dependencies
|
||||
- **Code check**: Validate code quality and guideline compliance
|
||||
- **Recording**: Record progress, questions, structure changes
|
||||
- **Generation**: Generate docs, code templates
|
||||
|
||||
### 3. Generate Command Content
|
||||
|
||||
Based on command type, generate appropriate content:
|
||||
|
||||
**Simple command** (1-3 lines):
|
||||
```markdown
|
||||
Concise instruction describing what to do
|
||||
```
|
||||
|
||||
**Complex command** (with steps):
|
||||
```markdown
|
||||
# Command Title
|
||||
|
||||
Command description
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. First Step
|
||||
Specific action
|
||||
|
||||
### 2. Second Step
|
||||
Specific action
|
||||
|
||||
## Output Format (if needed)
|
||||
|
||||
Template
|
||||
```
|
||||
|
||||
### 4. Create Files
|
||||
|
||||
Create in both directories:
|
||||
- `.cursor/commands/trellis-<command-name>.md`
|
||||
- `.opencode/commands/trellis/<command-name>.md`
|
||||
|
||||
### 5. Confirm Creation
|
||||
|
||||
Output result:
|
||||
```
|
||||
[OK] Created Slash Command: /<command-name>
|
||||
|
||||
File paths:
|
||||
- .cursor/commands/trellis-<command-name>.md
|
||||
- .opencode/commands/trellis/<command-name>.md
|
||||
|
||||
Usage:
|
||||
/trellis:<command-name>
|
||||
|
||||
Description:
|
||||
<description>
|
||||
```
|
||||
|
||||
## Command Content Guidelines
|
||||
|
||||
### [OK] Good command content
|
||||
|
||||
1. **Clear and concise**: Immediately understandable
|
||||
2. **Executable**: AI can follow steps directly
|
||||
3. **Well-scoped**: Clear boundaries of what to do and not do
|
||||
4. **Has output**: Specifies expected output format (if needed)
|
||||
|
||||
### [X] Avoid
|
||||
|
||||
1. **Too vague**: e.g., "optimize code"
|
||||
2. **Too complex**: Single command should not exceed 100 lines
|
||||
3. **Duplicate functionality**: Check if similar command exists first
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Command Type | Prefix | Example |
|
||||
|--------------|--------|---------|
|
||||
| Session Start | `start` | `start` |
|
||||
| Pre-development | `before-` | `before-dev` |
|
||||
| Check | `check-` | `check` |
|
||||
| Record | `record-` | `record-session` |
|
||||
| Generate | `generate-` | `generate-api-doc` |
|
||||
| Update | `update-` | `update-changelog` |
|
||||
| Other | Verb-first | `review-code`, `sync-data` |
|
||||
|
||||
## Example
|
||||
|
||||
### Input
|
||||
```
|
||||
/trellis:create-command review-pr Check PR code changes against project guidelines
|
||||
```
|
||||
|
||||
### Generated Command Content
|
||||
```markdown
|
||||
# PR Code Review
|
||||
|
||||
Check current PR code changes against project guidelines.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Get Changed Files
|
||||
```bash
|
||||
git diff main...HEAD --name-only
|
||||
```
|
||||
|
||||
### 2. Categorized Review
|
||||
|
||||
**Frontend files** (`apps/web/`):
|
||||
- Reference `.trellis/spec/frontend/index.md`
|
||||
|
||||
**Backend files** (`packages/api/`):
|
||||
- Reference `.trellis/spec/backend/index.md`
|
||||
|
||||
### 3. Output Review Report
|
||||
|
||||
Format:
|
||||
|
||||
## PR Review Report
|
||||
|
||||
### Changed Files
|
||||
- [file list]
|
||||
|
||||
### Check Results
|
||||
- [OK] Passed items
|
||||
- [X] Issues found
|
||||
|
||||
### Suggestions
|
||||
- [improvement suggestions]
|
||||
```
|
||||
144
.opencode/commands/trellis/finish-work.md
Normal file
144
.opencode/commands/trellis/finish-work.md
Normal file
@ -0,0 +1,144 @@
|
||||
# Finish Work - Pre-Commit Checklist
|
||||
|
||||
Before submitting or committing, use this checklist to ensure work completeness.
|
||||
|
||||
**Timing**: After code is written and tested, before commit
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
### 1. Code Quality
|
||||
|
||||
```bash
|
||||
# Must pass
|
||||
pnpm lint
|
||||
pnpm type-check
|
||||
pnpm test
|
||||
```
|
||||
|
||||
- [ ] `pnpm lint` passes with 0 errors?
|
||||
- [ ] `pnpm type-check` passes with no type errors?
|
||||
- [ ] Tests pass?
|
||||
- [ ] No `console.log` statements (use logger)?
|
||||
- [ ] No non-null assertions (the `x!` operator)?
|
||||
- [ ] No `any` types?
|
||||
|
||||
### 2. Code-Spec Sync
|
||||
|
||||
**Code-Spec Docs**:
|
||||
- [ ] Does `.trellis/spec/backend/` need updates?
|
||||
- New patterns, new modules, new conventions
|
||||
- [ ] Does `.trellis/spec/frontend/` need updates?
|
||||
- New components, new hooks, new patterns
|
||||
- [ ] Does `.trellis/spec/guides/` need updates?
|
||||
- New cross-layer flows, lessons from bugs
|
||||
|
||||
**Key Question**:
|
||||
> "If I fixed a bug or discovered something non-obvious, should I document it so future me (or others) won't hit the same issue?"
|
||||
|
||||
If YES -> Update the relevant code-spec doc.
|
||||
|
||||
### 2.5. Code-Spec Hard Block (Infra/Cross-Layer)
|
||||
|
||||
If this change touches infra or cross-layer contracts, this is a blocking checklist:
|
||||
|
||||
- [ ] Spec content is executable (real signatures/contracts), not principle-only text
|
||||
- [ ] Includes file path + command/API name + payload field names
|
||||
- [ ] Includes validation and error matrix
|
||||
- [ ] Includes Good/Base/Bad cases
|
||||
- [ ] Includes required tests and assertion points
|
||||
|
||||
**Block Rule**:
|
||||
In pipeline mode, the finish agent will automatically detect and execute spec updates when gaps are found.
|
||||
If running this checklist manually, ensure spec sync is complete before committing — run `/trellis:update-spec` if needed.
|
||||
|
||||
### 3. API Changes
|
||||
|
||||
If you modified API endpoints:
|
||||
|
||||
- [ ] Input schema updated?
|
||||
- [ ] Output schema updated?
|
||||
- [ ] API documentation updated?
|
||||
- [ ] Client code updated to match?
|
||||
|
||||
### 4. Database Changes
|
||||
|
||||
If you modified database schema:
|
||||
|
||||
- [ ] Migration file created?
|
||||
- [ ] Schema file updated?
|
||||
- [ ] Related queries updated?
|
||||
- [ ] Seed data updated (if applicable)?
|
||||
|
||||
### 5. Cross-Layer Verification
|
||||
|
||||
If the change spans multiple layers:
|
||||
|
||||
- [ ] Data flows correctly through all layers?
|
||||
- [ ] Error handling works at each boundary?
|
||||
- [ ] Types are consistent across layers?
|
||||
- [ ] Loading states handled?
|
||||
|
||||
### 6. Manual Testing
|
||||
|
||||
- [ ] Feature works in browser/app?
|
||||
- [ ] Edge cases tested?
|
||||
- [ ] Error states tested?
|
||||
- [ ] Works after page refresh?
|
||||
|
||||
---
|
||||
|
||||
## Quick Check Flow
|
||||
|
||||
```bash
|
||||
# 1. Code checks
|
||||
pnpm lint && pnpm type-check
|
||||
|
||||
# 2. View changes
|
||||
git status
|
||||
git diff --name-only
|
||||
|
||||
# 3. Based on changed files, check relevant items above
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Oversights
|
||||
|
||||
| Oversight | Consequence | Check |
|
||||
|-----------|-------------|-------|
|
||||
| Code-spec docs not updated | Others don't know the change | Check .trellis/spec/ |
|
||||
| Spec text is abstract only | Easy regressions in infra/cross-layer changes | Require signature/contract/matrix/cases/tests |
|
||||
| Migration not created | Schema out of sync | Check db/migrations/ |
|
||||
| Types not synced | Runtime errors | Check shared types |
|
||||
| Tests not updated | False confidence | Run full test suite |
|
||||
| Console.log left in | Noisy production logs | Search for console.log |
|
||||
|
||||
---
|
||||
|
||||
## Relationship to Other Commands
|
||||
|
||||
```
|
||||
Development Flow:
|
||||
Write code -> Test -> /trellis:finish-work -> git commit -> /trellis:record-session
|
||||
| |
|
||||
Ensure completeness Record progress
|
||||
|
||||
Debug Flow:
|
||||
Hit bug -> Fix -> /trellis:break-loop -> Knowledge capture
|
||||
|
|
||||
Deep analysis
|
||||
```
|
||||
|
||||
- `/trellis:finish-work` - Check work completeness (this command)
|
||||
- `/trellis:record-session` - Record session and commits
|
||||
- `/trellis:break-loop` - Deep analysis after debugging
|
||||
|
||||
---
|
||||
|
||||
## Core Principle
|
||||
|
||||
> **Delivery includes not just code, but also documentation, verification, and knowledge capture.**
|
||||
|
||||
Complete work = Code + Docs + Tests + Verification
|
||||
219
.opencode/commands/trellis/integrate-skill.md
Normal file
219
.opencode/commands/trellis/integrate-skill.md
Normal file
@ -0,0 +1,219 @@
|
||||
# Integrate Claude Skill into Project Guidelines
|
||||
|
||||
Adapt and integrate a Claude global skill into your project's development guidelines (not directly into project code).
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/trellis:integrate-skill <skill-name>
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
/trellis:integrate-skill frontend-design
|
||||
/trellis:integrate-skill mcp-builder
|
||||
```
|
||||
|
||||
## Core Principle
|
||||
|
||||
> [!] **Important**: The goal of skill integration is to update **development guidelines**, not to generate project code directly.
|
||||
>
|
||||
> - Guidelines content -> Write to `.trellis/spec/{target}/doc.md`
|
||||
> - Code examples -> Place in `.trellis/spec/{target}/examples/skills/<skill-name>/`
|
||||
> - Example files -> Use `.template` suffix (e.g., `component.tsx.template`) to avoid IDE errors
|
||||
>
|
||||
> Where `{target}` is `frontend` or `backend`, determined by skill type.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Read Skill Content
|
||||
|
||||
```bash
|
||||
openskills read <skill-name>
|
||||
```
|
||||
|
||||
If the skill doesn't exist, prompt user to check available skills:
|
||||
```bash
|
||||
# Available skills are listed in AGENTS.md under <available_skills>
|
||||
```
|
||||
|
||||
### 2. Determine Integration Target
|
||||
|
||||
Based on skill type, determine which guidelines to update:
|
||||
|
||||
| Skill Category | Integration Target |
|
||||
|----------------|-------------------|
|
||||
| UI/Frontend (`frontend-design`, `web-artifacts-builder`) | `.trellis/spec/frontend/` |
|
||||
| Backend/API (`mcp-builder`) | `.trellis/spec/backend/` |
|
||||
| Documentation (`doc-coauthoring`, `docx`, `pdf`) | `.trellis/` or create dedicated guidelines |
|
||||
| Testing (`webapp-testing`) | `.trellis/spec/frontend/` (E2E) |
|
||||
|
||||
### 3. Analyze Skill Content
|
||||
|
||||
Extract from the skill:
|
||||
- **Core concepts**: How the skill works and key concepts
|
||||
- **Best practices**: Recommended approaches
|
||||
- **Code patterns**: Reusable code templates
|
||||
- **Caveats**: Common issues and solutions
|
||||
|
||||
### 4. Execute Integration
|
||||
|
||||
#### 4.1 Update Guidelines Document
|
||||
|
||||
Add a new section to the corresponding `doc.md`:
|
||||
|
||||
```markdown
|
||||
@@@section:skill-<skill-name>
|
||||
## # <Skill Name> Integration Guide
|
||||
|
||||
### Overview
|
||||
[Core functionality and use cases of the skill]
|
||||
|
||||
### Project Adaptation
|
||||
[How to use this skill in the current project]
|
||||
|
||||
### Usage Steps
|
||||
1. [Step 1]
|
||||
2. [Step 2]
|
||||
|
||||
### Caveats
|
||||
- [Project-specific constraints]
|
||||
- [Differences from default behavior]
|
||||
|
||||
### Reference Examples
|
||||
See `examples/skills/<skill-name>/`
|
||||
|
||||
@@@/section:skill-<skill-name>
|
||||
```
|
||||
|
||||
#### 4.2 Create Examples Directory (if code examples exist)
|
||||
|
||||
```bash
|
||||
# Directory structure ({target} = frontend or backend)
|
||||
.trellis/spec/{target}/
|
||||
|-- doc.md # Add skill-related section
|
||||
|-- index.md # Update index
|
||||
+-- examples/
|
||||
+-- skills/
|
||||
+-- <skill-name>/
|
||||
|-- README.md # Example documentation
|
||||
|-- example-1.ts.template # Code example (use .template suffix)
|
||||
+-- example-2.tsx.template
|
||||
```
|
||||
|
||||
**File naming conventions**:
|
||||
- Code files: `<name>.<ext>.template` (e.g., `component.tsx.template`)
|
||||
- Config files: `<name>.config.template` (e.g., `tailwind.config.template`)
|
||||
- Documentation: `README.md` (normal suffix)
|
||||
|
||||
#### 4.3 Update Index File
|
||||
|
||||
Add to the Quick Navigation table in `index.md`:
|
||||
|
||||
```markdown
|
||||
| <Skill-related task> | <Section name> | `skill-<skill-name>` |
|
||||
```
|
||||
|
||||
### 5. Generate Integration Report
|
||||
|
||||
---
|
||||
|
||||
## Skill Integration Report: `<skill-name>`
|
||||
|
||||
### # Overview
|
||||
- **Skill description**: [Functionality description]
|
||||
- **Integration target**: `.trellis/spec/{target}/`
|
||||
|
||||
### # Tech Stack Compatibility
|
||||
|
||||
| Skill Requirement | Project Status | Compatibility |
|
||||
|-------------------|----------------|---------------|
|
||||
| [Tech 1] | [Project tech] | [OK]/[!]/[X] |
|
||||
|
||||
### # Integration Locations
|
||||
|
||||
| Type | Path |
|
||||
|------|------|
|
||||
| Guidelines doc | `.trellis/spec/{target}/doc.md` (section: `skill-<name>`) |
|
||||
| Code examples | `.trellis/spec/{target}/examples/skills/<name>/` |
|
||||
| Index update | `.trellis/spec/{target}/index.md` |
|
||||
|
||||
> `{target}` = `frontend` or `backend`
|
||||
|
||||
### # Dependencies (if needed)
|
||||
|
||||
```bash
|
||||
# Install required dependencies (adjust for your package manager)
|
||||
npm install <package>
|
||||
# or
|
||||
pnpm add <package>
|
||||
# or
|
||||
yarn add <package>
|
||||
```
|
||||
|
||||
### [OK] Completed Changes
|
||||
|
||||
- [ ] Added `@@@section:skill-<name>` section to `doc.md`
|
||||
- [ ] Added index entry to `index.md`
|
||||
- [ ] Created example files in `examples/skills/<name>/`
|
||||
- [ ] Example files use `.template` suffix
|
||||
|
||||
### # Related Guidelines
|
||||
|
||||
- [Existing related section IDs]
|
||||
|
||||
---
|
||||
|
||||
## 6. Optional: Create Usage Command
|
||||
|
||||
If this skill is frequently used, create a shortcut command:
|
||||
|
||||
```bash
|
||||
/trellis:create-command use-<skill-name> Use <skill-name> skill following project guidelines
|
||||
```
|
||||
|
||||
## Common Skill Integration Reference
|
||||
|
||||
| Skill | Integration Target | Examples Directory |
|
||||
|-------|-------------------|-------------------|
|
||||
| `frontend-design` | `frontend` | `examples/skills/frontend-design/` |
|
||||
| `mcp-builder` | `backend` | `examples/skills/mcp-builder/` |
|
||||
| `webapp-testing` | `frontend` | `examples/skills/webapp-testing/` |
|
||||
| `doc-coauthoring` | `.trellis/` | N/A (documentation workflow only) |
|
||||
|
||||
## Example: Integrating `mcp-builder` Skill
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
.trellis/spec/backend/
|
||||
|-- doc.md # Add MCP section
|
||||
|-- index.md # Add index entry
|
||||
+-- examples/
|
||||
+-- skills/
|
||||
+-- mcp-builder/
|
||||
|-- README.md
|
||||
|-- server.ts.template
|
||||
|-- tools.ts.template
|
||||
+-- types.ts.template
|
||||
```
|
||||
|
||||
### New Section in doc.md
|
||||
|
||||
```markdown
|
||||
@@@section:skill-mcp-builder
|
||||
## # MCP Server Development Guide
|
||||
|
||||
### Overview
|
||||
Create LLM-callable tool services using MCP (Model Context Protocol).
|
||||
|
||||
### Project Adaptation
|
||||
- Place services in a dedicated directory
|
||||
- Follow existing TypeScript and type definition conventions
|
||||
- Use project's logging system
|
||||
|
||||
### Reference Examples
|
||||
See `examples/skills/mcp-builder/`
|
||||
|
||||
@@@/section:skill-mcp-builder
|
||||
```
|
||||
0
.opencode/commands/trellis/migrate-specs.md
Normal file
0
.opencode/commands/trellis/migrate-specs.md
Normal file
358
.opencode/commands/trellis/onboard.md
Normal file
358
.opencode/commands/trellis/onboard.md
Normal file
@ -0,0 +1,358 @@
|
||||
You are a senior developer onboarding a new team member to this project's AI-assisted workflow system.
|
||||
|
||||
YOUR ROLE: Be a mentor and teacher. Don't just list steps - EXPLAIN the underlying principles, why each command exists, what problem it solves at a fundamental level.
|
||||
|
||||
## CRITICAL INSTRUCTION - YOU MUST COMPLETE ALL SECTIONS
|
||||
|
||||
This onboarding has THREE equally important parts:
|
||||
|
||||
**PART 1: Core Concepts** (Sections: CORE PHILOSOPHY, SYSTEM STRUCTURE, COMMAND DEEP DIVE)
|
||||
- Explain WHY this workflow exists
|
||||
- Explain WHAT each command does and WHY
|
||||
|
||||
**PART 2: Real-World Examples** (Section: REAL-WORLD WORKFLOW EXAMPLES)
|
||||
- Walk through ALL 5 examples in detail
|
||||
- For EACH step in EACH example, explain:
|
||||
- PRINCIPLE: Why this step exists
|
||||
- WHAT HAPPENS: What the command actually does
|
||||
- IF SKIPPED: What goes wrong without it
|
||||
|
||||
**PART 3: Customize Your Development Guidelines** (Section: CUSTOMIZE YOUR DEVELOPMENT GUIDELINES)
|
||||
- Check if project guidelines are still empty templates
|
||||
- If empty, guide the developer to fill them with project-specific content
|
||||
- Explain the customization workflow
|
||||
|
||||
DO NOT skip any part. All three parts are essential:
|
||||
- Part 1 teaches the concepts
|
||||
- Part 2 shows how concepts work in practice
|
||||
- Part 3 ensures the project has proper guidelines for AI to follow
|
||||
|
||||
After completing ALL THREE parts, ask the developer about their first task.
|
||||
|
||||
---
|
||||
|
||||
## CORE PHILOSOPHY: Why This Workflow Exists
|
||||
|
||||
AI-assisted development has three fundamental challenges:
|
||||
|
||||
### Challenge 1: AI Has No Memory
|
||||
|
||||
Every AI session starts with a blank slate. Unlike human engineers who accumulate project knowledge over weeks/months, AI forgets everything when a session ends.
|
||||
|
||||
**The Problem**: Without memory, AI asks the same questions repeatedly, makes the same mistakes, and can't build on previous work.
|
||||
|
||||
**The Solution**: The `.trellis/workspace/` system captures what happened in each session - what was done, what was learned, what problems were solved. The `/trellis:start` command reads this history at session start, giving AI "artificial memory."
|
||||
|
||||
### Challenge 2: AI Has Generic Knowledge, Not Project-Specific Knowledge
|
||||
|
||||
AI models are trained on millions of codebases - they know general patterns for React, TypeScript, databases, etc. But they don't know YOUR project's conventions.
|
||||
|
||||
**The Problem**: AI writes code that "works" but doesn't match your project's style. It uses patterns that conflict with existing code. It makes decisions that violate unwritten team rules.
|
||||
|
||||
**The Solution**: The `.trellis/spec/` directory contains project-specific guidelines. The `/before-*-dev` commands inject this specialized knowledge into AI context before coding starts.
|
||||
|
||||
### Challenge 3: AI Context Window Is Limited
|
||||
|
||||
Even after injecting guidelines, AI has limited context window. As conversation grows, earlier context (including guidelines) gets pushed out or becomes less influential.
|
||||
|
||||
**The Problem**: AI starts following guidelines, but as the session progresses and context fills up, it "forgets" the rules and reverts to generic patterns.
|
||||
|
||||
**The Solution**: The `/check-*` commands re-verify code against guidelines AFTER writing, catching drift that occurred during development. The `/trellis:finish-work` command does a final holistic review.
|
||||
|
||||
---
|
||||
|
||||
## SYSTEM STRUCTURE
|
||||
|
||||
```
|
||||
.trellis/
|
||||
|-- .developer # Your identity (gitignored)
|
||||
|-- workflow.md # Complete workflow documentation
|
||||
|-- workspace/ # "AI Memory" - session history
|
||||
| |-- index.md # All developers' progress
|
||||
| +-- {developer}/ # Per-developer directory
|
||||
| |-- index.md # Personal progress index
|
||||
| +-- journal-N.md # Session records (max 2000 lines)
|
||||
|-- tasks/ # Task tracking (unified)
|
||||
| +-- {MM}-{DD}-{slug}/ # Task directory
|
||||
| |-- task.json # Task metadata
|
||||
| +-- prd.md # Requirements doc
|
||||
|-- spec/ # "AI Training Data" - project knowledge
|
||||
| |-- frontend/ # Frontend conventions
|
||||
| |-- backend/ # Backend conventions
|
||||
| +-- guides/ # Thinking patterns
|
||||
+-- scripts/ # Automation tools
|
||||
```
|
||||
|
||||
### Understanding spec/ subdirectories
|
||||
|
||||
**frontend/** - Single-layer frontend knowledge:
|
||||
- Component patterns (how to write components in THIS project)
|
||||
- State management rules (Redux? Zustand? Context?)
|
||||
- Styling conventions (CSS modules? Tailwind? Styled-components?)
|
||||
- Hook patterns (custom hooks, data fetching)
|
||||
|
||||
**backend/** - Single-layer backend knowledge:
|
||||
- API design patterns (REST? GraphQL? tRPC?)
|
||||
- Database conventions (query patterns, migrations)
|
||||
- Error handling standards
|
||||
- Logging and monitoring rules
|
||||
|
||||
**guides/** - Cross-layer thinking guides:
|
||||
- Code reuse thinking guide
|
||||
- Cross-layer thinking guide
|
||||
- Pre-implementation checklists
|
||||
|
||||
---
|
||||
|
||||
## COMMAND DEEP DIVE
|
||||
|
||||
### /trellis:start - Restore AI Memory
|
||||
|
||||
**WHY IT EXISTS**:
|
||||
When a human engineer joins a project, they spend days/weeks learning: What is this project? What's been built? What's in progress? What's the current state?
|
||||
|
||||
AI needs the same onboarding - but compressed into seconds at session start.
|
||||
|
||||
**WHAT IT ACTUALLY DOES**:
|
||||
1. Reads developer identity (who am I in this project?)
|
||||
2. Checks git status (what branch? uncommitted changes?)
|
||||
3. Reads recent session history from `workspace/` (what happened before?)
|
||||
4. Identifies active features (what's in progress?)
|
||||
5. Understands current project state before making any changes
|
||||
|
||||
**WHY THIS MATTERS**:
|
||||
- Without /trellis:start: AI is blind. It might work on wrong branch, conflict with others' work, or redo already-completed work.
|
||||
- With /trellis:start: AI knows project context, can continue where previous session left off, avoids conflicts.
|
||||
|
||||
---
|
||||
|
||||
### /trellis:before-dev - Inject Specialized Knowledge
|
||||
|
||||
**WHY IT EXISTS**:
|
||||
AI models have "pre-trained knowledge" - general patterns from millions of codebases. But YOUR project has specific conventions that differ from generic patterns.
|
||||
|
||||
**WHAT IT ACTUALLY DOES**:
|
||||
1. Discovers spec layers via `get_context.py --mode packages` and reads relevant guidelines
|
||||
2. Loads project-specific patterns into AI's working context:
|
||||
- Component naming conventions
|
||||
- State management patterns
|
||||
- Database query patterns
|
||||
- Error handling standards
|
||||
|
||||
**WHY THIS MATTERS**:
|
||||
- Without before-dev: AI writes generic code that doesn't match project style.
|
||||
- With before-dev: AI writes code that looks like the rest of the codebase.
|
||||
|
||||
---
|
||||
|
||||
### /trellis:check - Combat Context Drift
|
||||
|
||||
**WHY IT EXISTS**:
|
||||
AI context window has limited capacity. As conversation progresses, guidelines injected at session start become less influential. This causes "context drift."
|
||||
|
||||
**WHAT IT ACTUALLY DOES**:
|
||||
1. Re-reads the guidelines that were injected earlier
|
||||
2. Compares written code against those guidelines
|
||||
3. Runs type checker and linter
|
||||
4. Identifies violations and suggests fixes
|
||||
|
||||
**WHY THIS MATTERS**:
|
||||
- Without check-*: Context drift goes unnoticed, code quality degrades.
|
||||
- With check-*: Drift is caught and corrected before commit.
|
||||
|
||||
---
|
||||
|
||||
### /trellis:check-cross-layer - Multi-Dimension Verification
|
||||
|
||||
**WHY IT EXISTS**:
|
||||
Most bugs don't come from lack of technical skill - they come from "didn't think of it":
|
||||
- Changed a constant in one place, missed 5 other places
|
||||
- Modified database schema, forgot to update the API layer
|
||||
- Created a utility function, but similar one already exists
|
||||
|
||||
**WHAT IT ACTUALLY DOES**:
|
||||
1. Identifies which dimensions your change involves
|
||||
2. For each dimension, runs targeted checks:
|
||||
- Cross-layer data flow
|
||||
- Code reuse analysis
|
||||
- Import path validation
|
||||
- Consistency checks
|
||||
|
||||
---
|
||||
|
||||
### /trellis:finish-work - Holistic Pre-Commit Review
|
||||
|
||||
**WHY IT EXISTS**:
|
||||
The `/check-*` commands focus on code quality within a single layer. But real changes often have cross-cutting concerns.
|
||||
|
||||
**WHAT IT ACTUALLY DOES**:
|
||||
1. Reviews all changes holistically
|
||||
2. Checks cross-layer consistency
|
||||
3. Identifies broader impacts
|
||||
4. Checks if new patterns should be documented
|
||||
|
||||
---
|
||||
|
||||
### /trellis:record-session - Persist Memory for Future
|
||||
|
||||
**WHY IT EXISTS**:
|
||||
All the context AI built during this session will be lost when session ends. The next session's `/trellis:start` needs this information.
|
||||
|
||||
**WHAT IT ACTUALLY DOES**:
|
||||
1. Records session summary to `workspace/{developer}/journal-N.md`
|
||||
2. Captures what was done, learned, and what's remaining
|
||||
3. Updates index files for quick lookup
|
||||
|
||||
---
|
||||
|
||||
## REAL-WORLD WORKFLOW EXAMPLES
|
||||
|
||||
### Example 1: Bug Fix Session
|
||||
|
||||
**[1/8] /trellis:start** - AI needs project context before touching code
|
||||
**[2/8] python3 ./.trellis/scripts/task.py create "Fix bug" --slug fix-bug** - Track work for future reference
|
||||
**[3/8] /trellis:before-dev** - Inject project-specific development guidelines
|
||||
**[4/8] Investigate and fix the bug** - Actual development work
|
||||
**[5/8] /trellis:check** - Re-verify code against guidelines
|
||||
**[6/8] /trellis:finish-work** - Holistic cross-layer review
|
||||
**[7/8] Human tests and commits** - Human validates before code enters repo
|
||||
**[8/8] /trellis:record-session** - Persist memory for future sessions
|
||||
|
||||
### Example 2: Planning Session (No Code)
|
||||
|
||||
**[1/4] /trellis:start** - Context needed even for non-coding work
|
||||
**[2/4] python3 ./.trellis/scripts/task.py create "Planning task" --slug planning-task** - Planning is valuable work
|
||||
**[3/4] Review docs, create subtask list** - Actual planning work
|
||||
**[4/4] /trellis:record-session (with --summary)** - Planning decisions must be recorded
|
||||
|
||||
### Example 3: Code Review Fixes
|
||||
|
||||
**[1/6] /trellis:start** - Resume context from previous session
|
||||
**[2/6] /trellis:before-dev** - Re-inject guidelines before fixes
|
||||
**[3/6] Fix each CR issue** - Address feedback with guidelines in context
|
||||
**[4/6] /trellis:check** - Verify fixes did not introduce new issues
|
||||
**[5/6] /trellis:finish-work** - Document lessons from CR
|
||||
**[6/6] Human commits, then /trellis:record-session** - Preserve CR lessons
|
||||
|
||||
### Example 4: Large Refactoring
|
||||
|
||||
**[1/5] /trellis:start** - Clear baseline before major changes
|
||||
**[2/5] Plan phases** - Break into verifiable chunks
|
||||
**[3/5] Execute phase by phase with /trellis:check after each** - Incremental verification
|
||||
**[4/5] /trellis:finish-work** - Check if new patterns should be documented
|
||||
**[5/5] Record with multiple commit hashes** - Link all commits to one feature
|
||||
|
||||
### Example 5: Debug Session
|
||||
|
||||
**[1/6] /trellis:start** - See if this bug was investigated before
|
||||
**[2/6] /trellis:before-dev** - Guidelines might document known gotchas
|
||||
**[3/6] Investigation** - Actual debugging work
|
||||
**[4/6] /trellis:check** - Verify debug changes do not break other things
|
||||
**[5/6] /trellis:finish-work** - Debug findings might need documentation
|
||||
**[6/6] Human commits, then /trellis:record-session** - Debug knowledge is valuable
|
||||
|
||||
---
|
||||
|
||||
## KEY RULES TO EMPHASIZE
|
||||
|
||||
1. **AI NEVER commits** - Human tests and approves. AI prepares, human validates.
|
||||
2. **Guidelines before code** - /before-dev command injects project knowledge.
|
||||
3. **Check after code** - /check-* commands catch context drift.
|
||||
4. **Record everything** - /trellis:record-session persists memory.
|
||||
|
||||
---
|
||||
|
||||
# PART 3: Customize Your Development Guidelines
|
||||
|
||||
After explaining Part 1 and Part 2, check if the project's development guidelines need customization.
|
||||
|
||||
## Step 1: Check Current Guidelines Status
|
||||
|
||||
Check if `.trellis/spec/` contains empty templates or customized guidelines:
|
||||
|
||||
```bash
|
||||
# Check if files are still empty templates (look for placeholder text)
|
||||
grep -l "To be filled by the team" .trellis/spec/backend/*.md 2>/dev/null | wc -l
|
||||
grep -l "To be filled by the team" .trellis/spec/frontend/*.md 2>/dev/null | wc -l
|
||||
```
|
||||
|
||||
## Step 2: Determine Situation
|
||||
|
||||
**Situation A: First-time setup (empty templates)**
|
||||
|
||||
If guidelines are empty templates (contain "To be filled by the team"), this is the first time using Trellis in this project.
|
||||
|
||||
Explain to the developer:
|
||||
|
||||
"I see that the development guidelines in `.trellis/spec/` are still empty templates. This is normal for a new Trellis setup!
|
||||
|
||||
The templates contain placeholder text that needs to be replaced with YOUR project's actual conventions. Without this, `/before-*-dev` commands won't provide useful guidance.
|
||||
|
||||
**Your first task should be to fill in these guidelines:**
|
||||
|
||||
1. Look at your existing codebase
|
||||
2. Identify the patterns and conventions already in use
|
||||
3. Document them in the guideline files
|
||||
|
||||
For example, for `.trellis/spec/backend/database-guidelines.md`:
|
||||
- What ORM/query library does your project use?
|
||||
- How are migrations managed?
|
||||
- What naming conventions for tables/columns?
|
||||
|
||||
Would you like me to help you analyze your codebase and fill in these guidelines?"
|
||||
|
||||
**Situation B: Guidelines already customized**
|
||||
|
||||
If guidelines have real content (no "To be filled" placeholders), this is an existing setup.
|
||||
|
||||
Explain to the developer:
|
||||
|
||||
"Great! Your team has already customized the development guidelines. You can start using `/before-*-dev` commands right away.
|
||||
|
||||
I recommend reading through `.trellis/spec/` to familiarize yourself with the team's coding standards."
|
||||
|
||||
## Step 3: Help Fill Guidelines (If Empty)
|
||||
|
||||
If the developer wants help filling guidelines, create a feature to track this:
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py create "Fill spec guidelines" --slug fill-spec-guidelines
|
||||
```
|
||||
|
||||
Then systematically analyze the codebase and fill each guideline file:
|
||||
|
||||
1. **Analyze the codebase** - Look at existing code patterns
|
||||
2. **Document conventions** - Write what you observe, not ideals
|
||||
3. **Include examples** - Reference actual files in the project
|
||||
4. **List forbidden patterns** - Document anti-patterns the team avoids
|
||||
|
||||
Work through one file at a time:
|
||||
- `backend/directory-structure.md`
|
||||
- `backend/database-guidelines.md`
|
||||
- `backend/error-handling.md`
|
||||
- `backend/quality-guidelines.md`
|
||||
- `backend/logging-guidelines.md`
|
||||
- `frontend/directory-structure.md`
|
||||
- `frontend/component-guidelines.md`
|
||||
- `frontend/hook-guidelines.md`
|
||||
- `frontend/state-management.md`
|
||||
- `frontend/quality-guidelines.md`
|
||||
- `frontend/type-safety.md`
|
||||
|
||||
---
|
||||
|
||||
## Completing the Onboard Session
|
||||
|
||||
After covering all three parts, summarize:
|
||||
|
||||
"You're now onboarded to the Trellis workflow system! Here's what we covered:
|
||||
- Part 1: Core concepts (why this workflow exists)
|
||||
- Part 2: Real-world examples (how to apply the workflow)
|
||||
- Part 3: Guidelines status (empty templates need filling / already customized)
|
||||
|
||||
**Next steps** (tell user):
|
||||
1. Run `/trellis:record-session` to record this onboard session
|
||||
2. [If guidelines empty] Start filling in `.trellis/spec/` guidelines
|
||||
3. [If guidelines ready] Start your first development task
|
||||
|
||||
What would you like to do first?"
|
||||
193
.opencode/commands/trellis/parallel.md
Normal file
193
.opencode/commands/trellis/parallel.md
Normal file
@ -0,0 +1,193 @@
|
||||
# Multi-Agent Pipeline Orchestrator
|
||||
|
||||
You are the Multi-Agent Pipeline Orchestrator Agent, running in the main repository, responsible for collaborating with users to manage parallel development tasks.
|
||||
|
||||
## Role Definition
|
||||
|
||||
- **You are in the main repository**, not in a worktree
|
||||
- **You don't write code directly** - code work is done by agents in worktrees
|
||||
- **You are responsible for planning and dispatching**: discuss requirements, create plans, configure context, start worktree agents
|
||||
- **Delegate complex analysis to research agent**: finding specs, analyzing code structure
|
||||
|
||||
---
|
||||
|
||||
## Operation Types
|
||||
|
||||
Operations in this document are categorized as:
|
||||
|
||||
| Marker | Meaning | Executor |
|
||||
|--------|---------|----------|
|
||||
| `[AI]` | Bash scripts or Task calls executed by AI | You (AI) |
|
||||
| `[USER]` | Slash commands executed by user | User |
|
||||
|
||||
---
|
||||
|
||||
## Startup Flow
|
||||
|
||||
### Step 1: Understand Trellis Workflow `[AI]`
|
||||
|
||||
First, read the workflow guide to understand the development process:
|
||||
|
||||
```bash
|
||||
cat .trellis/workflow.md # Development process, conventions, and quick start guide
|
||||
```
|
||||
|
||||
### Step 2: Get Current Status `[AI]`
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/get_context.py
|
||||
```
|
||||
|
||||
### Step 3: Read Project Guidelines `[AI]`
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/get_context.py --mode packages # Discover available spec layers
|
||||
cat .trellis/spec/guides/index.md # Thinking guides
|
||||
```
|
||||
|
||||
### Step 4: Ask User for Requirements
|
||||
|
||||
Ask the user:
|
||||
|
||||
1. What feature to develop?
|
||||
2. Which modules are involved?
|
||||
3. Development type? (backend / frontend / fullstack)
|
||||
|
||||
---
|
||||
|
||||
## Planning: Choose Your Approach
|
||||
|
||||
Based on requirement complexity, choose one of these approaches:
|
||||
|
||||
### Option A: Plan Agent (Recommended for complex features) `[AI]`
|
||||
|
||||
Use when:
|
||||
- Requirements need analysis and validation
|
||||
- Multiple modules or cross-layer changes
|
||||
- Unclear scope that needs research
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/multi_agent/plan.py \
|
||||
--name "<feature-name>" \
|
||||
--type "<backend|frontend|fullstack>" \
|
||||
--requirement "<user requirement description>" \
|
||||
--platform opencode
|
||||
```
|
||||
|
||||
Plan Agent will:
|
||||
1. Evaluate requirement validity (may reject if unclear/too large)
|
||||
2. Call research agent to analyze codebase
|
||||
3. Create and configure task directory
|
||||
4. Write prd.md with acceptance criteria
|
||||
5. Output ready-to-use task directory
|
||||
|
||||
After plan.py completes, start the worktree agent:
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/multi_agent/start.py "$TASK_DIR" --platform opencode
|
||||
```
|
||||
|
||||
### Option B: Manual Configuration (For simple/clear features) `[AI]`
|
||||
|
||||
Use when:
|
||||
- Requirements are already clear and specific
|
||||
- You know exactly which files are involved
|
||||
- Simple, well-scoped changes
|
||||
|
||||
#### Step 1: Create Task Directory
|
||||
|
||||
```bash
|
||||
# title is task description, --slug for task directory name
|
||||
TASK_DIR=$(python3 ./.trellis/scripts/task.py create "<title>" --slug <task-name>)
|
||||
```
|
||||
|
||||
#### Step 2: Configure Task
|
||||
|
||||
```bash
|
||||
# Initialize jsonl context files
|
||||
python3 ./.trellis/scripts/task.py init-context "$TASK_DIR" <dev_type>
|
||||
|
||||
# Set branch and scope
|
||||
python3 ./.trellis/scripts/task.py set-branch "$TASK_DIR" feature/<name>
|
||||
python3 ./.trellis/scripts/task.py set-scope "$TASK_DIR" <scope>
|
||||
```
|
||||
|
||||
#### Step 3: Add Context (optional: use research agent)
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" implement "<path>" "<reason>"
|
||||
python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" check "<path>" "<reason>"
|
||||
```
|
||||
|
||||
#### Step 4: Create prd.md
|
||||
|
||||
```bash
|
||||
cat > "$TASK_DIR/prd.md" << 'EOF'
|
||||
# Feature: <name>
|
||||
|
||||
## Requirements
|
||||
- ...
|
||||
|
||||
## Acceptance Criteria
|
||||
- ...
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Step 5: Validate and Start
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py validate "$TASK_DIR"
|
||||
python3 ./.trellis/scripts/multi_agent/start.py "$TASK_DIR" --platform opencode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## After Starting: Report Status
|
||||
|
||||
Tell the user the agent has started and provide monitoring commands.
|
||||
|
||||
---
|
||||
|
||||
## User Available Commands `[USER]`
|
||||
|
||||
The following slash commands are for users (not AI):
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/trellis:parallel` | Start Multi-Agent Pipeline (this command) |
|
||||
| `/trellis:start` | Start normal development mode (single process) |
|
||||
| `/trellis:record-session` | Record session progress |
|
||||
| `/trellis:finish-work` | Pre-completion checklist |
|
||||
|
||||
---
|
||||
|
||||
## Monitoring Commands (for user reference)
|
||||
|
||||
Tell the user they can use these commands to monitor:
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/multi_agent/status.py # Overview
|
||||
python3 ./.trellis/scripts/multi_agent/status.py --log <name> # View log
|
||||
python3 ./.trellis/scripts/multi_agent/status.py --watch <name> # Real-time monitoring
|
||||
python3 ./.trellis/scripts/multi_agent/cleanup.py <branch> # Cleanup worktree
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pipeline Phases
|
||||
|
||||
The dispatch agent in worktree will automatically execute:
|
||||
|
||||
1. implement → Implement feature
|
||||
2. check → Check code quality
|
||||
3. finish → Final verification
|
||||
4. create-pr → Create PR
|
||||
|
||||
---
|
||||
|
||||
## Core Rules
|
||||
|
||||
- **Don't write code directly** - delegate to agents in worktree
|
||||
- **Don't execute git commit** - agent does it via create-pr action
|
||||
- **Delegate complex analysis to research** - finding specs, analyzing code structure
|
||||
- **Subagents use globally configured model** - inherits from user's OpenCode config
|
||||
62
.opencode/commands/trellis/record-session.md
Normal file
62
.opencode/commands/trellis/record-session.md
Normal file
@ -0,0 +1,62 @@
|
||||
[!] **Prerequisite**: This command should only be used AFTER the human has tested and committed the code.
|
||||
|
||||
**Do NOT run `git commit` directly** — the scripts below handle their own commits for `.trellis/` metadata. You only need to read git history (`git log`, `git status`, `git diff`) and run the Python scripts.
|
||||
|
||||
---
|
||||
|
||||
## Record Work Progress
|
||||
|
||||
### Step 1: Get Context & Check Tasks
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/get_context.py --mode record
|
||||
```
|
||||
|
||||
[!] Archive tasks whose work is **actually done** — judge by work status, not the `status` field in task.json:
|
||||
- Code committed? → Archive it (don't wait for PR)
|
||||
- All acceptance criteria met? → Archive it
|
||||
- Don't skip archiving just because `status` still says `planning` or `in_progress`
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py archive <task-name>
|
||||
```
|
||||
|
||||
### Step 2: One-Click Add Session
|
||||
|
||||
```bash
|
||||
# Method 1: Simple parameters
|
||||
python3 ./.trellis/scripts/add_session.py \
|
||||
--title "Session Title" \
|
||||
--commit "hash1,hash2" \
|
||||
--summary "Brief summary of what was done"
|
||||
|
||||
# Method 2: Pass detailed content via stdin
|
||||
cat << 'EOF' | python3 ./.trellis/scripts/add_session.py --stdin --title "Title" --commit "hash"
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| New API | Added user authentication endpoint |
|
||||
| Frontend | Updated login form |
|
||||
|
||||
**Updated Files**:
|
||||
- `packages/api/modules/auth/router.ts`
|
||||
- `apps/web/modules/auth/components/login-form.tsx`
|
||||
EOF
|
||||
```
|
||||
|
||||
**Auto-completes**:
|
||||
- [OK] Appends session to journal-N.md
|
||||
- [OK] Auto-detects line count, creates new file if >2000 lines
|
||||
- [OK] Auto-detects Branch context (`--branch` override; otherwise Branch = task.json -> current git branch; missing values are omitted gracefully)
|
||||
- [OK] Updates index.md (Total Sessions +1, Last Active, line stats, history)
|
||||
- [OK] Auto-commits .trellis/workspace and .trellis/tasks changes
|
||||
|
||||
---
|
||||
|
||||
## Script Command Reference
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `python3 ./.trellis/scripts/get_context.py --mode record` | Get context for record-session |
|
||||
| `python3 ./.trellis/scripts/add_session.py --title "..." --commit "..."` | **One-click add session (recommended, branch auto-complete)** |
|
||||
| `python3 ./.trellis/scripts/task.py archive <name>` | Archive completed task (auto-commits) |
|
||||
| `python3 ./.trellis/scripts/task.py list` | List active tasks |
|
||||
351
.opencode/commands/trellis/start.md
Normal file
351
.opencode/commands/trellis/start.md
Normal file
@ -0,0 +1,351 @@
|
||||
# Start Session
|
||||
|
||||
Initialize your AI development session and begin working on tasks.
|
||||
|
||||
---
|
||||
|
||||
## Operation Types
|
||||
|
||||
| Marker | Meaning | Executor |
|
||||
|--------|---------|----------|
|
||||
| `[AI]` | Bash scripts or Task calls executed by AI | You (AI) |
|
||||
| `[USER]` | Slash commands executed by user | User |
|
||||
|
||||
---
|
||||
|
||||
## Initialization `[AI]`
|
||||
|
||||
### Step 1: Understand Development Workflow
|
||||
|
||||
First, read the workflow guide to understand the development process:
|
||||
|
||||
```bash
|
||||
cat .trellis/workflow.md
|
||||
```
|
||||
|
||||
**Follow the instructions in workflow.md** - it contains:
|
||||
- Core principles (Read Before Write, Follow Standards, etc.)
|
||||
- File system structure
|
||||
- Development process
|
||||
- Best practices
|
||||
|
||||
### Step 2: Get Current Context
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/get_context.py
|
||||
```
|
||||
|
||||
This shows: developer identity, git status, current task (if any), active tasks.
|
||||
|
||||
### Step 3: Read Guidelines Index
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/get_context.py --mode packages
|
||||
```
|
||||
|
||||
This shows available packages and their spec layers. Read the relevant spec indexes:
|
||||
|
||||
```bash
|
||||
cat .trellis/spec/<package>/<layer>/index.md # Package-specific guidelines
|
||||
cat .trellis/spec/guides/index.md # Thinking guides (always read)
|
||||
```
|
||||
|
||||
> **Important**: The index files are navigation — they list the actual guideline files (e.g., `error-handling.md`, `conventions.md`, `mock-strategies.md`).
|
||||
> At this step, just read the indexes to understand what's available.
|
||||
> When you start actual development, you MUST go back and read the specific guideline files relevant to your task, as listed in the index's Pre-Development Checklist.
|
||||
|
||||
### Step 4: Report and Ask
|
||||
|
||||
Report what you learned and ask: "What would you like to work on?"
|
||||
|
||||
---
|
||||
|
||||
## Task Classification
|
||||
|
||||
When user describes a task, classify it:
|
||||
|
||||
| Type | Criteria | Workflow |
|
||||
|------|----------|----------|
|
||||
| **Question** | User asks about code, architecture, or how something works | Answer directly |
|
||||
| **Trivial Fix** | Typo fix, comment update, single-line change, < 5 minutes | Direct Edit |
|
||||
| **Simple Task** | Clear goal, 1-2 files, well-defined scope | Quick confirm → Task Workflow |
|
||||
| **Complex Task** | Vague goal, multiple files, architectural decisions | **Brainstorm → Task Workflow** |
|
||||
|
||||
### Decision Rule
|
||||
|
||||
> **If in doubt, use Brainstorm + Task Workflow.**
|
||||
>
|
||||
> Task Workflow ensures code-spec context is injected to agents, resulting in higher quality code.
|
||||
> The overhead is minimal, but the benefit is significant.
|
||||
|
||||
---
|
||||
|
||||
## Question / Trivial Fix
|
||||
|
||||
For questions or trivial fixes, work directly:
|
||||
|
||||
1. Answer question or make the fix
|
||||
2. If code was changed, remind user to run `/trellis:finish-work`
|
||||
|
||||
---
|
||||
|
||||
## Complex Task - Brainstorm First
|
||||
|
||||
For complex or vague tasks, **automatically start the brainstorm process** — do NOT skip directly to implementation.
|
||||
|
||||
See `/trellis:brainstorm` for the full process. Summary:
|
||||
|
||||
1. **Acknowledge and classify** - State your understanding
|
||||
2. **Create task directory** - Track evolving requirements in `prd.md`
|
||||
3. **Ask questions one at a time** - Update PRD after each answer
|
||||
4. **Propose approaches** - For architectural decisions
|
||||
5. **Confirm final requirements** - Get explicit approval
|
||||
6. **Proceed to Task Workflow** - With clear requirements in PRD
|
||||
|
||||
> **Subtask Decomposition**: If brainstorm reveals multiple independent work items,
|
||||
> consider creating subtasks using `--parent` flag or `add-subtask` command.
|
||||
> See `/trellis:brainstorm` Step 8 for details.
|
||||
|
||||
---
|
||||
|
||||
## Task Workflow (Development Tasks)
|
||||
|
||||
**Why this workflow?**
|
||||
- Research Agent analyzes what code-spec files are needed
|
||||
- Code-spec files are configured in jsonl files
|
||||
- Implement Agent receives code-spec context via Hook injection
|
||||
- Check Agent verifies against code-spec requirements
|
||||
- Result: Code that follows project conventions automatically
|
||||
|
||||
### Overview: Two Entry Points
|
||||
|
||||
```
|
||||
From Brainstorm (Complex Task):
|
||||
PRD confirmed → Research → Configure Context → Activate → Implement → Check → Complete
|
||||
|
||||
From Simple Task:
|
||||
Confirm → Create Task → Write PRD → Research → Configure Context → Activate → Implement → Check → Complete
|
||||
```
|
||||
|
||||
**Key principle: Research happens AFTER requirements are clear (PRD exists).**
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: Establish Requirements
|
||||
|
||||
#### Path A: From Brainstorm (skip to Phase 2)
|
||||
|
||||
PRD and task directory already exist from brainstorm. Skip directly to Phase 2.
|
||||
|
||||
#### Path B: From Simple Task
|
||||
|
||||
**Step 1: Confirm Understanding** `[AI]`
|
||||
|
||||
Quick confirm:
|
||||
- What is the goal?
|
||||
- What type of development? (frontend / backend / fullstack)
|
||||
- Any specific requirements or constraints?
|
||||
|
||||
**Step 2: Create Task Directory** `[AI]`
|
||||
|
||||
```bash
|
||||
TASK_DIR=$(python3 ./.trellis/scripts/task.py create "<title>" --slug <name>)
|
||||
```
|
||||
|
||||
**Step 3: Write PRD** `[AI]`
|
||||
|
||||
Create `prd.md` in the task directory with:
|
||||
|
||||
```markdown
|
||||
# <Task Title>
|
||||
|
||||
## Goal
|
||||
<What we're trying to achieve>
|
||||
|
||||
## Requirements
|
||||
- <Requirement 1>
|
||||
- <Requirement 2>
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] <Criterion 1>
|
||||
- [ ] <Criterion 2>
|
||||
|
||||
## Technical Notes
|
||||
<Any technical decisions or constraints>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Prepare for Implementation (shared)
|
||||
|
||||
> Both paths converge here. PRD and task directory must exist before proceeding.
|
||||
|
||||
**Step 4: Code-Spec Depth Check** `[AI]`
|
||||
|
||||
If the task touches infra or cross-layer contracts, do not start implementation until code-spec depth is defined.
|
||||
|
||||
Trigger this requirement when the change includes any of:
|
||||
- New or changed command/API signatures
|
||||
- Database schema or migration changes
|
||||
- Infra integrations (storage, queue, cache, secrets, env contracts)
|
||||
- Cross-layer payload transformations
|
||||
|
||||
Must-have before proceeding:
|
||||
- [ ] Target code-spec files to update are identified
|
||||
- [ ] Concrete contract is defined (signature, fields, env keys)
|
||||
- [ ] Validation and error matrix is defined
|
||||
- [ ] At least one Good/Base/Bad case is defined
|
||||
|
||||
**Step 5: Research the Codebase** `[AI]`
|
||||
|
||||
Based on the confirmed PRD, call Research Agent to find relevant specs and patterns:
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "research",
|
||||
prompt: "Analyze the codebase for this task:
|
||||
|
||||
Task: <goal from PRD>
|
||||
Type: <frontend/backend/fullstack>
|
||||
|
||||
Please find:
|
||||
1. Relevant code-spec files in .trellis/spec/
|
||||
2. Existing code patterns to follow (find 2-3 examples)
|
||||
3. Files that will likely need modification
|
||||
|
||||
Output:
|
||||
## Relevant Code-Specs
|
||||
- <path>: <why it's relevant>
|
||||
|
||||
## Code Patterns Found
|
||||
- <pattern>: <example file path>
|
||||
|
||||
## Files to Modify
|
||||
- <path>: <what change>",
|
||||
model: "opus"
|
||||
)
|
||||
```
|
||||
|
||||
**Step 6: Configure Context** `[AI]`
|
||||
|
||||
Initialize default context:
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py init-context "$TASK_DIR" <type>
|
||||
# type: backend | frontend | fullstack
|
||||
```
|
||||
|
||||
Add code-spec files found by Research Agent:
|
||||
|
||||
```bash
|
||||
# For each relevant code-spec and code pattern:
|
||||
python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" implement "<path>" "<reason>"
|
||||
python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" check "<path>" "<reason>"
|
||||
```
|
||||
|
||||
**Step 7: Activate Task** `[AI]`
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py start "$TASK_DIR"
|
||||
```
|
||||
|
||||
This sets `.current-task` so hooks can inject context.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Execute (shared)
|
||||
|
||||
**Step 8: Implement** `[AI]`
|
||||
|
||||
Call Implement Agent (code-spec context is auto-injected by hook):
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "implement",
|
||||
prompt: "Implement the task described in prd.md.
|
||||
|
||||
Follow all code-spec files that have been injected into your context.
|
||||
Run lint and typecheck before finishing.",
|
||||
model: "opus"
|
||||
)
|
||||
```
|
||||
|
||||
**Step 9: Check Quality** `[AI]`
|
||||
|
||||
Call Check Agent (code-spec context is auto-injected by hook):
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "check",
|
||||
prompt: "Review all code changes against the code-spec requirements.
|
||||
|
||||
Fix any issues you find directly.
|
||||
Ensure lint and typecheck pass.",
|
||||
model: "opus"
|
||||
)
|
||||
```
|
||||
|
||||
**Step 10: Complete** `[AI]`
|
||||
|
||||
1. Verify lint and typecheck pass
|
||||
2. Report what was implemented
|
||||
3. Remind user to:
|
||||
- Test the changes
|
||||
- Commit when ready
|
||||
- Run `/trellis:record-session` to record this session
|
||||
|
||||
---
|
||||
|
||||
## Continuing Existing Task
|
||||
|
||||
If `get_context.py` shows a current task:
|
||||
|
||||
1. Read the task's `prd.md` to understand the goal
|
||||
2. Check `task.json` for current status and phase
|
||||
3. Ask user: "Continue working on <task-name>?"
|
||||
|
||||
If yes, resume from the appropriate step (usually Step 7 or 8).
|
||||
|
||||
---
|
||||
|
||||
## Commands Reference
|
||||
|
||||
### User Commands `[USER]`
|
||||
|
||||
| Command | When to Use |
|
||||
|---------|-------------|
|
||||
| `/trellis:start` | Begin a session (this command) |
|
||||
| `/trellis:brainstorm` | Clarify vague requirements (called from start) |
|
||||
| `/trellis:parallel` | Complex tasks needing isolated worktree |
|
||||
| `/trellis:finish-work` | Before committing changes |
|
||||
| `/trellis:record-session` | After completing a task |
|
||||
|
||||
### AI Scripts `[AI]`
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `python3 ./.trellis/scripts/get_context.py` | Get session context |
|
||||
| `python3 ./.trellis/scripts/task.py create` | Create task directory |
|
||||
| `python3 ./.trellis/scripts/task.py init-context` | Initialize jsonl files |
|
||||
| `python3 ./.trellis/scripts/task.py add-context` | Add code-spec/context file to jsonl |
|
||||
| `python3 ./.trellis/scripts/task.py start` | Set current task |
|
||||
| `python3 ./.trellis/scripts/task.py finish` | Clear current task |
|
||||
| `python3 ./.trellis/scripts/task.py archive` | Archive completed task |
|
||||
|
||||
### Sub Agents `[AI]`
|
||||
|
||||
| Agent | Purpose | Hook Injection |
|
||||
|-------|---------|----------------|
|
||||
| research | Analyze codebase | No (reads directly) |
|
||||
| implement | Write code | Yes (implement.jsonl) |
|
||||
| check | Review & fix | Yes (check.jsonl) |
|
||||
| debug | Fix specific issues | Yes (debug.jsonl) |
|
||||
|
||||
---
|
||||
|
||||
## Key Principle
|
||||
|
||||
> **Code-spec context is injected, not remembered.**
|
||||
>
|
||||
> The Task Workflow ensures agents receive relevant code-spec context automatically.
|
||||
> This is more reliable than hoping the AI "remembers" conventions.
|
||||
354
.opencode/commands/trellis/update-spec.md
Normal file
354
.opencode/commands/trellis/update-spec.md
Normal file
@ -0,0 +1,354 @@
|
||||
# Update Code-Spec - Capture Executable Contracts
|
||||
|
||||
When you learn something valuable (from debugging, implementing, or discussion), use this command 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 template download with giget | Relevant `backend/` or `frontend/` file |
|
||||
| **Made a design decision** | Used type field + mapping table for extensibility | Relevant code-spec + "Design Decisions" section |
|
||||
| **Fixed a bug** | Found a subtle issue with error handling | `backend/error-handling.md` |
|
||||
| **Discovered a pattern** | Found a better way to structure code | Relevant `backend/` or `frontend/` file |
|
||||
| **Hit a gotcha** | Learned that X must be done before Y | Relevant code-spec + "Common Mistakes" section |
|
||||
| **Established a convention** | Team agreed on naming pattern | `quality-guidelines.md` |
|
||||
| **New thinking trigger** | "Don't forget to check X before doing Y" | `guides/*.md` (as a checklist item, not detailed rules) |
|
||||
|
||||
**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/
|
||||
├── backend/ # Backend coding standards
|
||||
│ ├── index.md # Overview and links
|
||||
│ └── *.md # Topic-specific guidelines
|
||||
├── frontend/ # Frontend coding standards
|
||||
│ ├── 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** | `backend/*.md`, `frontend/*.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 `backend/` or `frontend/`
|
||||
- "This is **what to consider** before writing" → Put in `guides/`
|
||||
|
||||
**Example**:
|
||||
|
||||
| Learning | Wrong Location | Correct Location |
|
||||
|----------|----------------|------------------|
|
||||
| "Use `reconfigure()` not `TextIOWrapper` for Windows stdout" | ❌ `guides/cross-platform-thinking-guide.md` | ✅ `backend/script-conventions.md` |
|
||||
| "Remember to check encoding when writing cross-platform code" | ❌ `backend/script-conventions.md` | ✅ `guides/cross-platform-thinking-guide.md` |
|
||||
|
||||
**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 (this command)
|
||||
- `/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
|
||||
232
.opencode/lib/trellis-context.js
Normal file
232
.opencode/lib/trellis-context.js
Normal file
@ -0,0 +1,232 @@
|
||||
/**
|
||||
* Trellis Context Manager
|
||||
*
|
||||
* Utility class for OpenCode plugins providing file reading,
|
||||
* JSONL parsing, and context building capabilities.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, appendFileSync, readdirSync } from "fs"
|
||||
import { isAbsolute, join } from "path"
|
||||
import { platform } from "os"
|
||||
import { execSync } from "child_process"
|
||||
|
||||
const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
|
||||
// Debug logging
|
||||
const DEBUG_LOG = "/tmp/trellis-plugin-debug.log"
|
||||
|
||||
function debugLog(prefix, ...args) {
|
||||
const timestamp = new Date().toISOString()
|
||||
const msg = `[${timestamp}] [${prefix}] ${args.map(a => typeof a === "object" ? JSON.stringify(a) : a).join(" ")}\n`
|
||||
try {
|
||||
appendFileSync(DEBUG_LOG, msg)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trellis Context Manager
|
||||
*/
|
||||
export class TrellisContext {
|
||||
constructor(directory) {
|
||||
this.directory = directory
|
||||
debugLog("context", "TrellisContext initialized", { directory })
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Trellis Project Detection
|
||||
// ============================================================
|
||||
|
||||
isTrellisProject() {
|
||||
return existsSync(join(this.directory, ".trellis"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current task directory from .trellis/.current-task
|
||||
*/
|
||||
getCurrentTask() {
|
||||
try {
|
||||
const currentTaskPath = join(this.directory, ".trellis", ".current-task")
|
||||
if (!existsSync(currentTaskPath)) {
|
||||
return null
|
||||
}
|
||||
const taskRef = readFileSync(currentTaskPath, "utf-8").trim()
|
||||
const normalized = this.normalizeTaskRef(taskRef)
|
||||
return normalized || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
normalizeTaskRef(taskRef) {
|
||||
if (!taskRef) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (isAbsolute(taskRef)) {
|
||||
return taskRef.trim()
|
||||
}
|
||||
|
||||
let normalized = taskRef.trim().replace(/\\/g, "/")
|
||||
while (normalized.startsWith("./")) {
|
||||
normalized = normalized.slice(2)
|
||||
}
|
||||
|
||||
if (normalized.startsWith("tasks/")) {
|
||||
return `.trellis/${normalized}`
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
resolveTaskDir(taskRef) {
|
||||
const normalized = this.normalizeTaskRef(taskRef)
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isAbsolute(normalized)) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
if (normalized.startsWith(".trellis/")) {
|
||||
return join(this.directory, normalized)
|
||||
}
|
||||
|
||||
return join(this.directory, ".trellis", "tasks", normalized)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// File Reading Utilities
|
||||
// ============================================================
|
||||
|
||||
readFile(filePath) {
|
||||
try {
|
||||
if (existsSync(filePath)) {
|
||||
return readFileSync(filePath, "utf-8")
|
||||
}
|
||||
} catch {
|
||||
// Ignore read errors
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
readProjectFile(relativePath) {
|
||||
return this.readFile(join(this.directory, relativePath))
|
||||
}
|
||||
|
||||
runScript(scriptPath, cwd = null) {
|
||||
try {
|
||||
const result = execSync(`${PYTHON_CMD} "${scriptPath}"`, {
|
||||
cwd: cwd || this.directory,
|
||||
timeout: 10000,
|
||||
encoding: "utf-8",
|
||||
stdio: ["pipe", "pipe", "pipe"]
|
||||
})
|
||||
return result || ""
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// JSONL Reading
|
||||
// ============================================================
|
||||
|
||||
readDirectoryMdFiles(dirPath, maxFiles = 20) {
|
||||
const results = []
|
||||
const fullPath = join(this.directory, dirPath)
|
||||
|
||||
if (!existsSync(fullPath)) {
|
||||
return results
|
||||
}
|
||||
|
||||
try {
|
||||
const files = readdirSync(fullPath)
|
||||
.filter(f => f.endsWith(".md"))
|
||||
.sort()
|
||||
.slice(0, maxFiles)
|
||||
|
||||
for (const filename of files) {
|
||||
const filePath = join(dirPath, filename)
|
||||
const content = this.readProjectFile(filePath)
|
||||
if (content) {
|
||||
results.push({ path: filePath, content })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore directory read errors
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a JSONL file and load referenced files/directories
|
||||
* Supports:
|
||||
* {"file": "path/to/file.md", "reason": "..."}
|
||||
* {"file": "path/to/dir/", "type": "directory", "reason": "..."}
|
||||
*/
|
||||
readJsonlWithFiles(jsonlPath) {
|
||||
const results = []
|
||||
const content = this.readFile(jsonlPath)
|
||||
if (!content) return results
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
if (!line.trim()) continue
|
||||
try {
|
||||
const item = JSON.parse(line)
|
||||
const file = item.file || item.path
|
||||
const entryType = item.type || "file"
|
||||
|
||||
if (!file) continue
|
||||
|
||||
if (entryType === "directory") {
|
||||
const dirEntries = this.readDirectoryMdFiles(file)
|
||||
results.push(...dirEntries)
|
||||
} else {
|
||||
const fullPath = join(this.directory, file)
|
||||
const fileContent = this.readFile(fullPath)
|
||||
if (fileContent) {
|
||||
results.push({ path: file, content: fileContent })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors for individual lines
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
buildContextFromEntries(entries) {
|
||||
return entries.map(e => `=== ${e.path} ===\n${e.content}`).join("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Context Collector (for session deduplication)
|
||||
// ============================================================
|
||||
|
||||
class ContextCollector {
|
||||
constructor() {
|
||||
this.processed = new Set()
|
||||
}
|
||||
|
||||
markProcessed(sessionID) {
|
||||
this.processed.add(sessionID)
|
||||
}
|
||||
|
||||
isProcessed(sessionID) {
|
||||
return this.processed.has(sessionID)
|
||||
}
|
||||
|
||||
clear(sessionID) {
|
||||
this.processed.delete(sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const contextCollector = new ContextCollector()
|
||||
|
||||
// Export debug log for plugins
|
||||
export { debugLog }
|
||||
5
.opencode/package.json
Normal file
5
.opencode/package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.1.40"
|
||||
}
|
||||
}
|
||||
515
.opencode/plugins/inject-subagent-context.js
Normal file
515
.opencode/plugins/inject-subagent-context.js
Normal file
@ -0,0 +1,515 @@
|
||||
/**
|
||||
* Trellis Context Injection Plugin
|
||||
*
|
||||
* Injects context when Task tool is called with supported subagent types.
|
||||
* Uses OpenCode's tool.execute.before hook.
|
||||
*/
|
||||
|
||||
import { existsSync, writeFileSync, readdirSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { TrellisContext, debugLog } from "../lib/trellis-context.js"
|
||||
|
||||
// Supported subagent types
|
||||
const AGENTS_ALL = ["implement", "check", "debug", "research"]
|
||||
const AGENTS_REQUIRE_TASK = ["implement", "check", "debug"]
|
||||
// Agents that don't update phase (can be called at any time)
|
||||
const AGENTS_NO_PHASE_UPDATE = ["debug", "research"]
|
||||
|
||||
/**
|
||||
* Update current_phase in task.json based on subagent_type
|
||||
*/
|
||||
function updateCurrentPhase(ctx, taskDir, subagentType) {
|
||||
if (AGENTS_NO_PHASE_UPDATE.includes(subagentType)) {
|
||||
return
|
||||
}
|
||||
|
||||
const taskJsonPath = join(ctx.directory, taskDir, "task.json")
|
||||
const content = ctx.readFile(taskJsonPath)
|
||||
if (!content) return
|
||||
|
||||
try {
|
||||
const taskData = JSON.parse(content)
|
||||
const currentPhase = taskData.current_phase || 0
|
||||
const nextActions = taskData.next_action || []
|
||||
|
||||
const actionToAgent = {
|
||||
"implement": "implement",
|
||||
"check": "check",
|
||||
"finish": "check"
|
||||
}
|
||||
|
||||
let newPhase = null
|
||||
for (const action of nextActions) {
|
||||
const phaseNum = action.phase || 0
|
||||
const actionName = action.action || ""
|
||||
const expectedAgent = actionToAgent[actionName]
|
||||
|
||||
if (phaseNum > currentPhase && expectedAgent === subagentType) {
|
||||
newPhase = phaseNum
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (newPhase !== null) {
|
||||
taskData.current_phase = newPhase
|
||||
writeFileSync(taskJsonPath, JSON.stringify(taskData, null, 2))
|
||||
debugLog("inject", "Updated current_phase to:", newPhase)
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog("inject", "Error updating phase:", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context for implement agent
|
||||
*/
|
||||
function getImplementContext(ctx, taskDir) {
|
||||
const parts = []
|
||||
|
||||
let jsonlPath = join(ctx.directory, taskDir, "implement.jsonl")
|
||||
let entries = ctx.readJsonlWithFiles(jsonlPath)
|
||||
|
||||
if (entries.length === 0) {
|
||||
jsonlPath = join(ctx.directory, taskDir, "spec.jsonl")
|
||||
entries = ctx.readJsonlWithFiles(jsonlPath)
|
||||
}
|
||||
|
||||
if (entries.length > 0) {
|
||||
parts.push(ctx.buildContextFromEntries(entries))
|
||||
}
|
||||
|
||||
const prd = ctx.readProjectFile(join(taskDir, "prd.md"))
|
||||
if (prd) {
|
||||
parts.push(`=== ${taskDir}/prd.md (Requirements) ===\n${prd}`)
|
||||
}
|
||||
|
||||
const info = ctx.readProjectFile(join(taskDir, "info.md"))
|
||||
if (info) {
|
||||
parts.push(`=== ${taskDir}/info.md (Technical Design) ===\n${info}`)
|
||||
}
|
||||
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context for check agent
|
||||
*/
|
||||
function getCheckContext(ctx, taskDir) {
|
||||
const parts = []
|
||||
|
||||
const jsonlPath = join(ctx.directory, taskDir, "check.jsonl")
|
||||
const entries = ctx.readJsonlWithFiles(jsonlPath)
|
||||
|
||||
if (entries.length > 0) {
|
||||
parts.push(ctx.buildContextFromEntries(entries))
|
||||
} else {
|
||||
const checkFiles = [
|
||||
[".opencode/commands/trellis/finish-work.md", "Finish work checklist"],
|
||||
[".opencode/commands/trellis/check-cross-layer.md", "Cross-layer check spec"],
|
||||
[".opencode/commands/trellis/check.md", "Check spec"],
|
||||
]
|
||||
for (const [f, description] of checkFiles) {
|
||||
const content = ctx.readProjectFile(f)
|
||||
if (content) {
|
||||
parts.push(`=== ${f} (${description}) ===\n${content}`)
|
||||
}
|
||||
}
|
||||
|
||||
const specJsonlPath = join(ctx.directory, taskDir, "spec.jsonl")
|
||||
const specEntries = ctx.readJsonlWithFiles(specJsonlPath)
|
||||
for (const entry of specEntries) {
|
||||
parts.push(`=== ${entry.path} (Dev spec) ===\n${entry.content}`)
|
||||
}
|
||||
}
|
||||
|
||||
const prd = ctx.readProjectFile(join(taskDir, "prd.md"))
|
||||
if (prd) {
|
||||
parts.push(`=== ${taskDir}/prd.md (Requirements - for understanding intent) ===\n${prd}`)
|
||||
}
|
||||
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context for finish phase (final check before PR)
|
||||
*/
|
||||
function getFinishContext(ctx, taskDir) {
|
||||
const parts = []
|
||||
|
||||
const jsonlPath = join(ctx.directory, taskDir, "finish.jsonl")
|
||||
const entries = ctx.readJsonlWithFiles(jsonlPath)
|
||||
|
||||
if (entries.length > 0) {
|
||||
parts.push(ctx.buildContextFromEntries(entries))
|
||||
} else {
|
||||
const finishWork = ctx.readProjectFile(".opencode/commands/trellis/finish-work.md")
|
||||
if (finishWork) {
|
||||
parts.push(`=== .opencode/commands/trellis/finish-work.md (Finish checklist) ===\n${finishWork}`)
|
||||
}
|
||||
}
|
||||
|
||||
const updateSpec = ctx.readProjectFile(".opencode/commands/trellis/update-spec.md")
|
||||
if (updateSpec) {
|
||||
parts.push(`=== .opencode/commands/trellis/update-spec.md (Spec update process) ===\n${updateSpec}`)
|
||||
}
|
||||
|
||||
const prd = ctx.readProjectFile(join(taskDir, "prd.md"))
|
||||
if (prd) {
|
||||
parts.push(`=== ${taskDir}/prd.md (Requirements - verify all met) ===\n${prd}`)
|
||||
}
|
||||
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context for debug agent
|
||||
*/
|
||||
function getDebugContext(ctx, taskDir) {
|
||||
const parts = []
|
||||
|
||||
const jsonlPath = join(ctx.directory, taskDir, "debug.jsonl")
|
||||
const entries = ctx.readJsonlWithFiles(jsonlPath)
|
||||
|
||||
if (entries.length > 0) {
|
||||
parts.push(ctx.buildContextFromEntries(entries))
|
||||
} else {
|
||||
const specJsonlPath = join(ctx.directory, taskDir, "spec.jsonl")
|
||||
const specEntries = ctx.readJsonlWithFiles(specJsonlPath)
|
||||
for (const entry of specEntries) {
|
||||
parts.push(`=== ${entry.path} (Dev spec) ===\n${entry.content}`)
|
||||
}
|
||||
|
||||
const checkFiles = [
|
||||
[".opencode/commands/trellis/check.md", "Check spec"],
|
||||
[".opencode/commands/trellis/check-cross-layer.md", "Cross-layer check spec"],
|
||||
]
|
||||
for (const [f, description] of checkFiles) {
|
||||
const content = ctx.readProjectFile(f)
|
||||
if (content) {
|
||||
parts.push(`=== ${f} (${description}) ===\n${content}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const codex = ctx.readProjectFile(join(taskDir, "codex-review-output.txt"))
|
||||
if (codex) {
|
||||
parts.push(`=== ${taskDir}/codex-review-output.txt (Codex Review Results) ===\n${codex}`)
|
||||
}
|
||||
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context for research agent
|
||||
*/
|
||||
function getResearchContext(ctx, taskDir) {
|
||||
const parts = []
|
||||
|
||||
// Dynamic project structure (scan actual spec directory)
|
||||
const specPath = ".trellis/spec"
|
||||
const specFull = join(ctx.directory, specPath)
|
||||
|
||||
const structureLines = [`## Project Spec Directory Structure\n\n\`\`\`\n${specPath}/`]
|
||||
if (existsSync(specFull)) {
|
||||
try {
|
||||
const entries = readdirSync(specFull, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory() && !d.name.startsWith("."))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = join(specFull, entry.name)
|
||||
if (existsSync(join(entryPath, "index.md"))) {
|
||||
structureLines.push(`├── ${entry.name}/`)
|
||||
} else {
|
||||
try {
|
||||
const nested = readdirSync(entryPath, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory() && existsSync(join(entryPath, d.name, "index.md")))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
if (nested.length > 0) {
|
||||
structureLines.push(`├── ${entry.name}/`)
|
||||
for (const n of nested) {
|
||||
structureLines.push(`│ ├── ${n.name}/`)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore nested read errors
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore read errors
|
||||
}
|
||||
}
|
||||
structureLines.push("```")
|
||||
|
||||
parts.push(structureLines.join("\n") + `
|
||||
|
||||
## Search Tips
|
||||
|
||||
- Spec files: \`.trellis/spec/**/*.md\`
|
||||
- Known issues: \`.trellis/big-question/\`
|
||||
- Code search: Use Glob and Grep tools
|
||||
- Tech solutions: Use mcp__exa__web_search_exa or mcp__exa__get_code_context_exa`)
|
||||
|
||||
if (taskDir) {
|
||||
const jsonlPath = join(ctx.directory, taskDir, "research.jsonl")
|
||||
const researchEntries = ctx.readJsonlWithFiles(jsonlPath)
|
||||
if (researchEntries.length > 0) {
|
||||
parts.push("\n## Additional Search Context\n")
|
||||
parts.push(ctx.buildContextFromEntries(researchEntries))
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Build enhanced prompt with context
|
||||
*/
|
||||
function buildPrompt(agentType, originalPrompt, context, isFinish = false) {
|
||||
const templates = {
|
||||
implement: `# Implement Agent Task
|
||||
|
||||
You are the Implement Agent in the Multi-Agent Pipeline.
|
||||
|
||||
## Your Context
|
||||
|
||||
${context}
|
||||
|
||||
---
|
||||
|
||||
## Your Task
|
||||
|
||||
${originalPrompt}
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Understand specs** - All dev specs are injected above
|
||||
2. **Understand requirements** - Read requirements and technical design
|
||||
3. **Implement feature** - Follow specs and design
|
||||
4. **Self-check** - Ensure code quality
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- Do NOT execute git commit
|
||||
- Follow all dev specs injected above
|
||||
- Report list of modified/created files when done`,
|
||||
|
||||
check: isFinish ? `# Finish Agent Task
|
||||
|
||||
You are performing the final check before creating a PR.
|
||||
|
||||
## Your Context
|
||||
|
||||
${context}
|
||||
|
||||
---
|
||||
|
||||
## Your Task
|
||||
|
||||
${originalPrompt}
|
||||
|
||||
---
|
||||
|
||||
## 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` :
|
||||
`# Check Agent Task
|
||||
|
||||
You are the Check Agent in the Multi-Agent Pipeline.
|
||||
|
||||
## Your Context
|
||||
|
||||
${context}
|
||||
|
||||
---
|
||||
|
||||
## Your Task
|
||||
|
||||
${originalPrompt}
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Get changes** - Run \`git diff --name-only\` and \`git diff\`
|
||||
2. **Check against specs** - Check item by item
|
||||
3. **Self-fix** - Fix issues directly, don't just report
|
||||
4. **Run verification** - Run lint and typecheck
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- Fix issues yourself, don't just report
|
||||
- Must execute complete checklist`,
|
||||
|
||||
debug: `# Debug Agent Task
|
||||
|
||||
You are the Debug Agent in the Multi-Agent Pipeline.
|
||||
|
||||
## Your Context
|
||||
|
||||
${context}
|
||||
|
||||
---
|
||||
|
||||
## Your Task
|
||||
|
||||
${originalPrompt}
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Understand issues** - Analyze issues pointed out
|
||||
2. **Locate code** - Find positions that need fixing
|
||||
3. **Fix against specs** - Fix following dev specs
|
||||
4. **Verify fixes** - Run typecheck
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- Do NOT execute git commit
|
||||
- Run typecheck after each fix`,
|
||||
|
||||
research: `# Research Agent Task
|
||||
|
||||
You are the Research Agent in the Multi-Agent Pipeline.
|
||||
|
||||
## Core Principle
|
||||
|
||||
**You do one thing: find and explain information.**
|
||||
|
||||
## Project Info
|
||||
|
||||
${context}
|
||||
|
||||
---
|
||||
|
||||
## Your Task
|
||||
|
||||
${originalPrompt}
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Understand query** - Determine search type and scope
|
||||
2. **Plan search** - List search steps
|
||||
3. **Execute search** - Run multiple searches in parallel
|
||||
4. **Organize results** - Output structured report
|
||||
|
||||
## Strict Boundaries
|
||||
|
||||
**Only allowed**: Describe what exists, where it is, how it works
|
||||
|
||||
**Forbidden**: Suggest improvements, criticize implementation, modify files`
|
||||
}
|
||||
|
||||
return templates[agentType] || originalPrompt
|
||||
}
|
||||
|
||||
export default {
|
||||
id: "trellis.inject-subagent-context",
|
||||
server: async ({ directory }) => {
|
||||
const ctx = new TrellisContext(directory)
|
||||
debugLog("inject", "Plugin loaded, directory:", directory)
|
||||
|
||||
return {
|
||||
"tool.execute.before": async (input, output) => {
|
||||
try {
|
||||
debugLog("inject", "tool.execute.before called, tool:", input?.tool)
|
||||
|
||||
const toolName = input?.tool?.toLowerCase()
|
||||
if (toolName !== "task") {
|
||||
return
|
||||
}
|
||||
|
||||
const args = output?.args
|
||||
if (!args) return
|
||||
|
||||
const subagentType = args.subagent_type
|
||||
const originalPrompt = args.prompt || ""
|
||||
|
||||
debugLog("inject", "Task tool called, subagent_type:", subagentType)
|
||||
|
||||
if (!AGENTS_ALL.includes(subagentType)) {
|
||||
debugLog("inject", "Skipping - unsupported subagent_type")
|
||||
return
|
||||
}
|
||||
|
||||
// Read current task
|
||||
const taskDir = ctx.getCurrentTask()
|
||||
|
||||
// Agents requiring task directory
|
||||
if (AGENTS_REQUIRE_TASK.includes(subagentType)) {
|
||||
if (!taskDir) {
|
||||
debugLog("inject", "Skipping - no current task")
|
||||
return
|
||||
}
|
||||
const taskDirFull = join(directory, taskDir)
|
||||
if (!existsSync(taskDirFull)) {
|
||||
debugLog("inject", "Skipping - task directory not found")
|
||||
return
|
||||
}
|
||||
|
||||
updateCurrentPhase(ctx, taskDir, subagentType)
|
||||
}
|
||||
|
||||
// Check for [finish] marker
|
||||
const isFinish = originalPrompt.toLowerCase().includes("[finish]")
|
||||
|
||||
// Get context based on agent type
|
||||
let context = ""
|
||||
switch (subagentType) {
|
||||
case "implement":
|
||||
context = getImplementContext(ctx, taskDir)
|
||||
break
|
||||
case "check":
|
||||
context = isFinish
|
||||
? getFinishContext(ctx, taskDir)
|
||||
: getCheckContext(ctx, taskDir)
|
||||
break
|
||||
case "debug":
|
||||
context = getDebugContext(ctx, taskDir)
|
||||
break
|
||||
case "research":
|
||||
context = getResearchContext(ctx, taskDir)
|
||||
break
|
||||
}
|
||||
|
||||
if (!context) {
|
||||
debugLog("inject", "No context to inject")
|
||||
return
|
||||
}
|
||||
|
||||
const newPrompt = buildPrompt(subagentType, originalPrompt, context, isFinish)
|
||||
|
||||
// Mutate args in-place — whole-object replacement does NOT work for the task tool
|
||||
// because the runtime holds a local reference to the same args object.
|
||||
args.prompt = newPrompt
|
||||
|
||||
debugLog("inject", "Injected context for", subagentType, "prompt length:", newPrompt.length)
|
||||
|
||||
} catch (error) {
|
||||
debugLog("inject", "Error in tool.execute.before:", error.message, error.stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
477
.opencode/plugins/session-start.js
Normal file
477
.opencode/plugins/session-start.js
Normal file
@ -0,0 +1,477 @@
|
||||
/* global process */
|
||||
/**
|
||||
* Trellis Session Start Plugin
|
||||
*
|
||||
* Injects context when user sends the first message in a session.
|
||||
* Uses OpenCode's chat.message hook directly so the context persists in history.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "fs"
|
||||
import { basename, join } from "path"
|
||||
import { execFileSync } from "child_process"
|
||||
import { platform } from "os"
|
||||
import { TrellisContext, contextCollector, debugLog } from "../lib/trellis-context.js"
|
||||
|
||||
const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
|
||||
|
||||
|
||||
/**
|
||||
* Check current task status and return structured status string.
|
||||
* JavaScript equivalent of _get_task_status in Claude's session-start.py.
|
||||
*/
|
||||
function getTaskStatus(ctx) {
|
||||
const taskRef = ctx.getCurrentTask()
|
||||
if (!taskRef) {
|
||||
return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
|
||||
}
|
||||
|
||||
const taskDir = ctx.resolveTaskDir(taskRef)
|
||||
|
||||
if (!taskDir || !existsSync(taskDir)) {
|
||||
return `Status: STALE POINTER\nTask: ${taskRef}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish`
|
||||
}
|
||||
|
||||
let taskData = {}
|
||||
const taskJsonPath = join(taskDir, "task.json")
|
||||
if (existsSync(taskJsonPath)) {
|
||||
try {
|
||||
taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8"))
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
const taskTitle = taskData.title || taskRef
|
||||
const taskStatus = taskData.status || "unknown"
|
||||
|
||||
if (taskStatus === "completed") {
|
||||
const dirName = basename(taskDir)
|
||||
return `Status: COMPLETED\nTask: ${taskTitle}\nNext: Archive with \`python3 ./.trellis/scripts/task.py archive ${dirName}\` or start a new task`
|
||||
}
|
||||
|
||||
let hasContext = false
|
||||
for (const jsonlName of ["implement.jsonl", "check.jsonl", "spec.jsonl"]) {
|
||||
const jsonlPath = join(taskDir, jsonlName)
|
||||
if (existsSync(jsonlPath)) {
|
||||
try {
|
||||
const st = statSync(jsonlPath)
|
||||
if (st.size > 0) {
|
||||
hasContext = true
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
// Ignore stat errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasPrd = existsSync(join(taskDir, "prd.md"))
|
||||
|
||||
if (!hasPrd) {
|
||||
return `Status: NOT READY\nTask: ${taskTitle}\nMissing: prd.md not created\nNext: Write PRD, then research → init-context → start`
|
||||
}
|
||||
|
||||
if (!hasContext) {
|
||||
return `Status: NOT READY\nTask: ${taskTitle}\nMissing: Context not configured (no jsonl files)\nNext: Complete Phase 2 (research → init-context → start) before implementing`
|
||||
}
|
||||
|
||||
return `Status: READY\nTask: ${taskTitle}\nNext: Continue with implement or check`
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Trellis config for session-start decisions.
|
||||
* Calls get_context.py --mode packages --json for reliable config data.
|
||||
*/
|
||||
function loadTrellisConfig(directory) {
|
||||
const scriptPath = join(directory, ".trellis", "scripts", "get_context.py")
|
||||
if (!existsSync(scriptPath)) {
|
||||
return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
|
||||
}
|
||||
try {
|
||||
const output = execFileSync(PYTHON_CMD, [scriptPath, "--mode", "packages", "--json"], {
|
||||
cwd: directory,
|
||||
timeout: 5000,
|
||||
encoding: "utf-8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
})
|
||||
const data = JSON.parse(output)
|
||||
if (data.mode !== "monorepo") {
|
||||
return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
|
||||
}
|
||||
const pkgDict = {}
|
||||
for (const pkg of (data.packages || [])) {
|
||||
pkgDict[pkg.name] = pkg
|
||||
}
|
||||
return {
|
||||
isMonorepo: true,
|
||||
packages: pkgDict,
|
||||
specScope: data.specScope || null,
|
||||
activeTaskPackage: data.activeTaskPackage || null,
|
||||
defaultPackage: data.defaultPackage || null,
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog("session", "loadTrellisConfig error:", e.message)
|
||||
return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check for legacy spec directory structure in monorepo.
|
||||
*/
|
||||
function checkLegacySpec(directory, config) {
|
||||
if (!config.isMonorepo || Object.keys(config.packages).length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const specDir = join(directory, ".trellis", "spec")
|
||||
if (!existsSync(specDir)) return null
|
||||
|
||||
let hasLegacy = false
|
||||
for (const name of ["backend", "frontend"]) {
|
||||
if (existsSync(join(specDir, name, "index.md"))) {
|
||||
hasLegacy = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasLegacy) return null
|
||||
|
||||
const pkgNames = Object.keys(config.packages).sort()
|
||||
const missing = pkgNames.filter(name => !existsSync(join(specDir, name)))
|
||||
|
||||
if (missing.length === 0) return null
|
||||
|
||||
if (missing.length === pkgNames.length) {
|
||||
return (
|
||||
`[!] Legacy spec structure detected: found \`spec/backend/\` or \`spec/frontend/\` ` +
|
||||
`but no package-scoped \`spec/<package>/\` directories.\n` +
|
||||
`Monorepo packages: ${pkgNames.join(", ")}\n` +
|
||||
`Please reorganize: \`spec/backend/\` -> \`spec/<package>/backend/\``
|
||||
)
|
||||
}
|
||||
return (
|
||||
`[!] Partial spec migration detected: packages ${missing.join(", ")} ` +
|
||||
`still missing \`spec/<pkg>/\` directory.\n` +
|
||||
`Please complete migration for all packages.`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolve which packages should have their specs injected.
|
||||
* Returns a Set of allowed package names, or null for full scan.
|
||||
*/
|
||||
function resolveSpecScope(config) {
|
||||
if (!config.isMonorepo || Object.keys(config.packages).length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { specScope, activeTaskPackage, defaultPackage, packages } = config
|
||||
if (specScope == null) return null
|
||||
|
||||
if (specScope === "active_task") {
|
||||
if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage])
|
||||
if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage])
|
||||
return null
|
||||
}
|
||||
|
||||
if (Array.isArray(specScope)) {
|
||||
const valid = new Set()
|
||||
for (const entry of specScope) {
|
||||
if (entry in packages) {
|
||||
valid.add(entry)
|
||||
}
|
||||
}
|
||||
if (valid.size > 0) return valid
|
||||
if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage])
|
||||
if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage])
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build session context for injection
|
||||
*/
|
||||
function buildSessionContext(ctx) {
|
||||
const directory = ctx.directory
|
||||
const trellisDir = join(directory, ".trellis")
|
||||
|
||||
const config = loadTrellisConfig(directory)
|
||||
const allowedPkgs = resolveSpecScope(config)
|
||||
|
||||
const parts = []
|
||||
|
||||
// 1. Header
|
||||
parts.push(`<trellis-context>
|
||||
You are starting a new session in a Trellis-managed project.
|
||||
Read and follow all instructions below carefully.
|
||||
</trellis-context>`)
|
||||
|
||||
// Legacy migration warning
|
||||
const legacyWarning = checkLegacySpec(directory, config)
|
||||
if (legacyWarning) {
|
||||
parts.push(`<migration-warning>\n${legacyWarning}\n</migration-warning>`)
|
||||
}
|
||||
|
||||
// 2. Current Context (dynamic)
|
||||
const contextScript = join(trellisDir, "scripts", "get_context.py")
|
||||
if (existsSync(contextScript)) {
|
||||
const output = ctx.runScript(contextScript)
|
||||
if (output) {
|
||||
parts.push("<current-state>")
|
||||
parts.push(output)
|
||||
parts.push("</current-state>")
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Workflow Guide (ToC only — lazy-load the full file on demand)
|
||||
const workflowContent = ctx.readProjectFile(".trellis/workflow.md")
|
||||
if (workflowContent) {
|
||||
const tocLines = [
|
||||
"# Development Workflow — Section Index",
|
||||
"Full guide: .trellis/workflow.md (read on demand)",
|
||||
"",
|
||||
]
|
||||
for (const line of workflowContent.split("\n")) {
|
||||
if (line.startsWith("## ")) tocLines.push(line)
|
||||
}
|
||||
tocLines.push("", "To read a section: use the Read tool on .trellis/workflow.md")
|
||||
parts.push("<workflow>")
|
||||
parts.push(tocLines.join("\n"))
|
||||
parts.push("</workflow>")
|
||||
}
|
||||
|
||||
// 4. Guidelines Index (dynamic discovery, matching Claude's session-start.py)
|
||||
parts.push("<guidelines>")
|
||||
parts.push("**Note**: The guidelines below are index files — they list available guideline documents and their locations.")
|
||||
parts.push("During actual development, you MUST read the specific guideline files listed in each index's Pre-Development Checklist.\n")
|
||||
|
||||
const specDir = join(directory, ".trellis", "spec")
|
||||
if (existsSync(specDir)) {
|
||||
try {
|
||||
const subs = readdirSync(specDir).filter(name => {
|
||||
if (name.startsWith(".")) return false
|
||||
try {
|
||||
return statSync(join(specDir, name)).isDirectory()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}).sort()
|
||||
|
||||
for (const sub of subs) {
|
||||
if (sub === "guides") {
|
||||
const indexFile = join(specDir, sub, "index.md")
|
||||
if (existsSync(indexFile)) {
|
||||
const content = ctx.readFile(indexFile)
|
||||
if (content) {
|
||||
parts.push(`## ${sub}\n${content}\n`)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const indexFile = join(specDir, sub, "index.md")
|
||||
if (existsSync(indexFile)) {
|
||||
const content = ctx.readFile(indexFile)
|
||||
if (content) {
|
||||
parts.push(`## ${sub}\n${content}\n`)
|
||||
}
|
||||
} else {
|
||||
if (allowedPkgs !== null && !allowedPkgs.has(sub)) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const nested = readdirSync(join(specDir, sub)).filter(name => {
|
||||
try {
|
||||
return statSync(join(specDir, sub, name)).isDirectory()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}).sort()
|
||||
|
||||
for (const layer of nested) {
|
||||
const nestedIndex = join(specDir, sub, layer, "index.md")
|
||||
if (existsSync(nestedIndex)) {
|
||||
const content = ctx.readFile(nestedIndex)
|
||||
if (content) {
|
||||
parts.push(`## ${sub}/${layer}\n${content}\n`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore directory read errors
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore spec directory read errors
|
||||
}
|
||||
}
|
||||
|
||||
parts.push("</guidelines>")
|
||||
|
||||
// 6. Task status
|
||||
const taskStatus = getTaskStatus(ctx)
|
||||
parts.push(`<task-status>\n${taskStatus}\n</task-status>`)
|
||||
|
||||
// 7. Final directive
|
||||
parts.push(`<ready>
|
||||
Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them.
|
||||
Wait for the user's first message, then handle it following the workflow guide.
|
||||
If there is an active task, ask whether to continue it.
|
||||
</ready>`)
|
||||
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
function getTrellisMetadata(metadata) {
|
||||
if (!metadata || typeof metadata !== "object") {
|
||||
return {}
|
||||
}
|
||||
|
||||
const trellis = metadata.trellis
|
||||
if (!trellis || typeof trellis !== "object") {
|
||||
return {}
|
||||
}
|
||||
|
||||
return trellis
|
||||
}
|
||||
|
||||
function markPartAsSessionStart(part) {
|
||||
const metadata = part.metadata && typeof part.metadata === "object"
|
||||
? part.metadata
|
||||
: {}
|
||||
part.metadata = {
|
||||
...metadata,
|
||||
trellis: {
|
||||
...getTrellisMetadata(metadata),
|
||||
sessionStart: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function hasSessionStartMarker(part) {
|
||||
if (!part || part.type !== "text" || typeof part.text !== "string") {
|
||||
return false
|
||||
}
|
||||
|
||||
return getTrellisMetadata(part.metadata).sessionStart === true
|
||||
}
|
||||
|
||||
export function hasInjectedTrellisContext(messages) {
|
||||
if (!Array.isArray(messages)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return messages.some(message => {
|
||||
if (!message?.info || message.info.role !== "user" || !Array.isArray(message.parts)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return message.parts.some(hasSessionStartMarker)
|
||||
})
|
||||
}
|
||||
|
||||
async function hasPersistedInjectedContext(client, directory, sessionID) {
|
||||
try {
|
||||
const response = await client.session.messages({
|
||||
path: { id: sessionID },
|
||||
query: { directory },
|
||||
throwOnError: true,
|
||||
})
|
||||
return hasInjectedTrellisContext(response.data || [])
|
||||
} catch (error) {
|
||||
debugLog(
|
||||
"session",
|
||||
"Failed to read session history for dedupe:",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
id: "trellis.session-start",
|
||||
server: async ({ directory, client }) => {
|
||||
const ctx = new TrellisContext(directory)
|
||||
debugLog("session", "Plugin loaded, directory:", directory)
|
||||
|
||||
return {
|
||||
// Clear in-memory dedupe after compaction so context can be re-injected.
|
||||
event: ({ event }) => {
|
||||
try {
|
||||
if (event?.type === "session.compacted" && event?.properties?.sessionID) {
|
||||
const sessionID = event.properties.sessionID
|
||||
contextCollector.clear(sessionID)
|
||||
debugLog("session", "Cleared processed flag after compaction for session:", sessionID)
|
||||
}
|
||||
} catch (error) {
|
||||
debugLog(
|
||||
"session",
|
||||
"Error in event hook:",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
// chat.message - triggered when user sends a message.
|
||||
// Modify the message in-place so the context is persisted with updateMessage/updatePart.
|
||||
"chat.message": async (input, output) => {
|
||||
try {
|
||||
const sessionID = input.sessionID
|
||||
const agent = input.agent || "unknown"
|
||||
debugLog("session", "chat.message called, sessionID:", sessionID, "agent:", agent)
|
||||
|
||||
// Skip in non-interactive mode
|
||||
if (process.env.OPENCODE_NON_INTERACTIVE === "1") {
|
||||
debugLog("session", "Skipping - non-interactive mode")
|
||||
return
|
||||
}
|
||||
|
||||
// Only inject on first message
|
||||
if (contextCollector.isProcessed(sessionID)) {
|
||||
debugLog("session", "Skipping - session already processed")
|
||||
return
|
||||
}
|
||||
|
||||
if (await hasPersistedInjectedContext(client, ctx.directory, sessionID)) {
|
||||
contextCollector.markProcessed(sessionID)
|
||||
debugLog("session", "Skipping - session already contains persisted Trellis context")
|
||||
return
|
||||
}
|
||||
|
||||
// Build context
|
||||
const context = buildSessionContext(ctx)
|
||||
debugLog("session", "Built context, length:", context.length)
|
||||
|
||||
// Inject context directly into output.parts so it gets persisted by updatePart
|
||||
const parts = output?.parts || []
|
||||
const textPartIndex = parts.findIndex(
|
||||
p => p.type === "text" && p.text !== undefined
|
||||
)
|
||||
|
||||
if (textPartIndex !== -1) {
|
||||
const originalText = parts[textPartIndex].text || ""
|
||||
parts[textPartIndex].text = `${context}\n\n---\n\n${originalText}`
|
||||
markPartAsSessionStart(parts[textPartIndex])
|
||||
debugLog("session", "Injected context into chat.message text part, length:", context.length)
|
||||
} else {
|
||||
// No existing text part: prepend a new one
|
||||
const injectedPart = { type: "text", text: context }
|
||||
markPartAsSessionStart(injectedPart)
|
||||
parts.unshift(injectedPart)
|
||||
debugLog("session", "Prepended new text part with context, length:", context.length)
|
||||
}
|
||||
|
||||
contextCollector.markProcessed(sessionID)
|
||||
|
||||
} catch (error) {
|
||||
debugLog("session", "Error in chat.message:", error.message, error.stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
.trellis/.gitignore
vendored
Normal file
29
.trellis/.gitignore
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
# Developer identity (local only)
|
||||
.developer
|
||||
|
||||
# Current task pointer (each dev works on different task)
|
||||
.current-task
|
||||
|
||||
# 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
|
||||
66
.trellis/.template-hashes.json
Normal file
66
.trellis/.template-hashes.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
".trellis/config.yaml": "cb5216826e1091bfa0d4ebae257d6f43fa60ca2bafbf3d9a6489b2c1a51a99df",
|
||||
".trellis/scripts/__init__.py": "1242be5b972094c2e141aecbe81a4efd478f6534e3d5e28306374e6a18fcf46c",
|
||||
".trellis/scripts/add_session.py": "a97a6c88ff7def8045a5dffa5c698a823392d7f73c1641e8a0c08db0168bd913",
|
||||
".trellis/scripts/common/__init__.py": "a8afa14ebe662723f96e4f5757c15359d76adf4cb5c52327c94dbe854bd1ab01",
|
||||
".trellis/scripts/common/cli_adapter.py": "d9bc50508918c5a2e17ce48384de5e1d9f7c2e32b0d57019d1bec973728d445f",
|
||||
".trellis/scripts/common/config.py": "1244bccc9d3e1e1c304f914ea913f2f4a0b6d54571e6845633d586c2139a07bf",
|
||||
".trellis/scripts/common/developer.py": "f5f833123abe68890171b4da825a324216d24913f6b5ad9245afc556424ffd7b",
|
||||
".trellis/scripts/common/git.py": "e14817be7de122d3a106f509c2825aeb9669d962ba73ba241642d2931cfdf1d6",
|
||||
".trellis/scripts/common/git_context.py": "5a01f7209c4a85ec768c88e2cb2a02a40ff279ea26139a390d16bf7576efce9f",
|
||||
".trellis/scripts/common/io.py": "6480b181f2bc505323b28ed7a66963d7b7edc96251e83b4c8e7a45907cc721c8",
|
||||
".trellis/scripts/common/log.py": "471df6895cfac80f995edebbf9974f6b7440634b7a688f28b8331c868bc0f3cf",
|
||||
".trellis/scripts/common/packages_context.py": "efe158d7c99c2268851d0216fbb08de22836e418a8dbeb73575b8cc249eed7b7",
|
||||
".trellis/scripts/common/paths.py": "36f72bdc09e4f0db53250346a4744ff3699c634ea71380eed5b467095f3d946b",
|
||||
".trellis/scripts/common/phase.py": "412b7096ef0e48b8a95a79060121a586e0d9d44f1b350d6ed818c6f84330bb01",
|
||||
".trellis/scripts/common/registry.py": "e1f4480ec264734eb4ef6867edc42fce2e2535045e5cff835fb81e0443d673e6",
|
||||
".trellis/scripts/common/session_context.py": "2389eff1a66b172783fcb714a79385114d9b29746133a3e0db732c3b5cb23898",
|
||||
".trellis/scripts/common/task_context.py": "8aad7107a7f86e7ddc381a7358364a6176c61d619ee2094cb698c24a0d5ab214",
|
||||
".trellis/scripts/common/task_queue.py": "0be61f713462b1fe4574927c82fc4704e678afe72dcb9813543aedf2f9e9e0c5",
|
||||
".trellis/scripts/common/task_store.py": "79e6257a0bedfd854fbfb5bdb3898a81181bcd700bc03ae3a970a032f7793610",
|
||||
".trellis/scripts/common/task_utils.py": "f5ef4af87ba3e11d8b19630c0c96d009de1811fc9be56c2027a9c96e21ed103e",
|
||||
".trellis/scripts/common/tasks.py": "eeefae693dadec54c8945394e288e90ed1e8f79545dfb2d4934a431496f5229d",
|
||||
".trellis/scripts/common/types.py": "d623ada858d3cb55093c5c95c9ce182b99b5865b7441742297542fad3b21cc06",
|
||||
".trellis/scripts/common/worktree.py": "434880e02dfa2e92f0c717ed2a28e4cdee681ea10c329a2438d533bdbc612408",
|
||||
".trellis/scripts/create_bootstrap.py": "33b40df671ba7828fd8d3ba8c019823a8b03e938797b1cae218c55d6c7ebe57a",
|
||||
".trellis/scripts/get_context.py": "ca5bf9e90bdb1d75d3de182b95f820f9d108ab28793d29097b24fd71315adcf5",
|
||||
".trellis/scripts/get_developer.py": "84c27076323c3e0f2c9c8ed16e8aa865e225d902a187c37e20ee1a46e7142d8f",
|
||||
".trellis/scripts/hooks/linear_sync.py": "e09cc4ce4699aada908808718698f33f705a3edf55c4dcf8f777ad892f80ca79",
|
||||
".trellis/scripts/init_developer.py": "f9e6c0d882406e81c8cd6b1c5abb204b0befc0069ff89cf650cd536a80f8c60e",
|
||||
".trellis/scripts/multi_agent/__init__.py": "af6fceb4d9a64da04be03ba0f5a6daf71066503eca832b8b58d8a7d4b2844fa4",
|
||||
".trellis/scripts/multi_agent/_bootstrap.py": "4d0c06e41ba17e33172974783719731551607400de0c751c13414fed9d0c8c30",
|
||||
".trellis/scripts/multi_agent/cleanup.py": "046ad29aa533e41d8952bf02c2dcfcdab2755002222d92455d194ef97a6e82e2",
|
||||
".trellis/scripts/multi_agent/create_pr.py": "03018c0c50a45758c28da5751afea1822be0acffe9053587cdf9d514a83ae27e",
|
||||
".trellis/scripts/multi_agent/plan.py": "011481ad0024a91f6a9e16535c6f5c4c7ba8eb311c46428104f1aeea7fc934e7",
|
||||
".trellis/scripts/multi_agent/start.py": "01e7f81fac53078a24a9fda50dc3000c2cee49179e05b8f7284747eebec1a543",
|
||||
".trellis/scripts/multi_agent/status.py": "06b3d2c5a9f7bea884962ace3b25113a3b01bf50dc12ad2f473e4f0c914fff7e",
|
||||
".trellis/scripts/multi_agent/status_display.py": "d432446644b07dcbea7fd3aeba1d31ae42a9c664e91eebbdab503fbb698bccac",
|
||||
".trellis/scripts/multi_agent/status_monitor.py": "11ba35180a568aa4d14ccaee81cc213ff3d5ab83025f264ac57ca70385f11f4c",
|
||||
".trellis/scripts/task.py": "77cd33a8c58803cd7dbe64bb9c045d6c93c1fa1453bed1e286d6f62bc329607e",
|
||||
".trellis/workflow.md": "892242e83f53194f49f7326ed9b50a7fc72ff810cd316c4d03ea8e7f11f5bbbd",
|
||||
".trellis/worktree.yaml": "c57de79e40d5f748f099625ed4a17d5f0afbf25cac598aced0b3c964e7b7c226",
|
||||
".opencode/agents/check.md": "39763ef458f95a2b38c0fbc9cd79df8c66909086e0ace4e4e80c536f58d09aed",
|
||||
".opencode/agents/debug.md": "0bac1d723fb3634ea95c471a22245eff2b4c9d6bd98bc66cafacf6a0092609bb",
|
||||
".opencode/agents/dispatch.md": "f1e90f5967c7c4d61fce58df2a1e8592d1bfd4e76f2d1761916b8dc3b0b435b9",
|
||||
".opencode/agents/implement.md": "6dcf015cec12f004dd97d13ee3c904f9c6af9fb30f57f30a33b1db38a0c37725",
|
||||
".opencode/agents/research.md": "6238919a015391970f30f806b9fb6f3b34f3a211733b728104652e717c35576e",
|
||||
".opencode/agents/trellis-plan.md": "36de06c7eddbff290acb3c200f30af96291048e492ce2f2d8b7038662eeb572b",
|
||||
".opencode/commands/trellis/before-dev.md": "dd926596f3139c12d42469fb5147ac90724e3a7baca5591384f4f4bbdd530b54",
|
||||
".opencode/commands/trellis/brainstorm.md": "7c7731eda092275a5d87f2569a69584f3c39b544a126a76e727a1e9d250c4a65",
|
||||
".opencode/commands/trellis/break-loop.md": "ba4dd4022dde1e4bbcfc1cc99e6a118e51b9db95bd962d88f1c29d0c9c433112",
|
||||
".opencode/commands/trellis/check-cross-layer.md": "b9ab24515ead84330d6634f6ad912ca3547db3a36139d62c5688161824097d60",
|
||||
".opencode/commands/trellis/check.md": "8b0d20b425b6030d13ac5aa0c876c5ec97cf7aca9b050f574f07f281ad25bd06",
|
||||
".opencode/commands/trellis/create-command.md": "b3c3ad4e34113cf67af8a94ac78d7d32078e93d318c23a1c27596944b4cb2c1d",
|
||||
".opencode/commands/trellis/finish-work.md": "dd147ab880063f4359322618f39ac0e84e1494aa9f67883dcd82e947f6b5d8bb",
|
||||
".opencode/commands/trellis/integrate-skill.md": "3940442485341832257c595ddfb45582e2d60e5a4716f2bd15b7bce0498b130a",
|
||||
".opencode/commands/trellis/migrate-specs.md": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
".opencode/commands/trellis/onboard.md": "cf9591fcddc412ff80772bf441c8d94d7724e6713fdf38a04a3348ab8949e64e",
|
||||
".opencode/commands/trellis/parallel.md": "9c383b9622c6bedc45f4184cc05f73a0e5087a1d88072e31ad84703d07a14c70",
|
||||
".opencode/commands/trellis/record-session.md": "33b5626fcf03a57578f46133b2a14c6bbe19c4ef29652af3f828f24f448f5926",
|
||||
".opencode/commands/trellis/start.md": "84e76dea69542515e234f8b551491ca6bb31fe441ef702a43ff12c3b8397d5fe",
|
||||
".opencode/commands/trellis/update-spec.md": "ff4d5a0405a763e61936f5b9df175fd25ea20ec5c20fa999855020ab78a919b6",
|
||||
".opencode/lib/trellis-context.js": "a752ed614d31ba15fd445aa61b1751d45bfd583b3d7a096d250e84222901e317",
|
||||
".opencode/package.json": "6867538b2f6025286a70c943d15db4191d2412daa8bf7431a846b00c863f26b8",
|
||||
".opencode/plugins/inject-subagent-context.js": "01a25c036bc4340b66fefce47c87b7ddb0f54ca27557edaae1e769e5e82bf4ea",
|
||||
".opencode/plugins/session-start.js": "c5fa1b619ff3b28c3a014001913530216436501d84008b0464d65ff30b17925c"
|
||||
}
|
||||
1
.trellis/.version
Normal file
1
.trellis/.version
Normal file
@ -0,0 +1 @@
|
||||
0.4.0
|
||||
53
.trellis/config.yaml
Normal file
53
.trellis/config.yaml
Normal file
@ -0,0 +1,53 @@
|
||||
# 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
|
||||
|
||||
# Default package used when --package is not specified.
|
||||
# default_package: frontend
|
||||
5
.trellis/scripts/__init__.py
Executable file
5
.trellis/scripts/__init__.py
Executable file
@ -0,0 +1,5 @@
|
||||
"""
|
||||
Trellis Python Scripts
|
||||
|
||||
This module provides Python implementations of Trellis workflow scripts.
|
||||
"""
|
||||
521
.trellis/scripts/add_session.py
Executable file
521
.trellis/scripts/add_session.py
Executable file
@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Add a new session to journal file and update index.md.
|
||||
|
||||
Usage:
|
||||
python3 add_session.py --title "Title" --commit "hash" --summary "Summary" [--package cli]
|
||||
python3 add_session.py --title "Title" --branch "feat/my-branch"
|
||||
|
||||
# Pipe detailed content via stdin (use --stdin to opt in):
|
||||
cat << 'EOF' | python3 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())
|
||||
84
.trellis/scripts/common/__init__.py
Executable file
84
.trellis/scripts/common/__init__.py
Executable file
@ -0,0 +1,84 @@
|
||||
"""
|
||||
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,
|
||||
)
|
||||
753
.trellis/scripts/common/cli_adapter.py
Executable file
753
.trellis/scripts/common/cli_adapter.py
Executable file
@ -0,0 +1,753 @@
|
||||
"""
|
||||
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, and Factory Droid 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)
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
|
||||
@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', or '.codebuddy')
|
||||
"""
|
||||
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"
|
||||
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, or .codebuddy)
|
||||
"""
|
||||
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
|
||||
Claude/OpenCode use subdirectory: .claude/commands/trellis/<name>.md
|
||||
"""
|
||||
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/<name>/SKILL.md
|
||||
Kiro: .kiro/skills/<name>/SKILL.md
|
||||
Gemini: .gemini/commands/trellis/<name>.toml
|
||||
Antigravity: .agent/workflows/<name>.md
|
||||
Windsurf: .windsurf/workflows/trellis-<name>.md
|
||||
Others: .{platform}/commands/trellis/<name>.md
|
||||
"""
|
||||
if self.platform == "cursor":
|
||||
return f".cursor/commands/trellis-{name}.md"
|
||||
elif self.platform == "codex":
|
||||
return f".agents/skills/{name}/SKILL.md"
|
||||
elif self.platform == "kiro":
|
||||
return f".kiro/skills/{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"
|
||||
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 {}
|
||||
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 integrated with Trellis multi-agent."
|
||||
)
|
||||
|
||||
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 integrated with Trellis multi-agent."
|
||||
)
|
||||
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"
|
||||
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")
|
||||
|
||||
@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', or 'codebuddy')
|
||||
|
||||
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",
|
||||
):
|
||||
raise ValueError(
|
||||
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', or 'droid')"
|
||||
)
|
||||
|
||||
return CLIAdapter(platform=platform) # type: ignore
|
||||
|
||||
|
||||
_ALL_PLATFORM_CONFIG_DIRS = (
|
||||
".claude",
|
||||
".cursor",
|
||||
".iflow",
|
||||
".opencode",
|
||||
".agents",
|
||||
".codex",
|
||||
".kilocode",
|
||||
".kiro",
|
||||
".gemini",
|
||||
".agent",
|
||||
".windsurf",
|
||||
".qoder",
|
||||
".codebuddy",
|
||||
".github/copilot",
|
||||
".factory",
|
||||
)
|
||||
"""All platform config directory names (used by detect_platform exclusion checks)."""
|
||||
|
||||
|
||||
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. Default → claude
|
||||
|
||||
Args:
|
||||
project_root: Project root directory
|
||||
|
||||
Returns:
|
||||
Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 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",
|
||||
):
|
||||
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"
|
||||
|
||||
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)
|
||||
264
.trellis/scripts/common/config.py
Executable file
264
.trellis/scripts/common/config.py
Executable file
@ -0,0 +1,264 @@
|
||||
#!/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
|
||||
from .worktree import parse_simple_yaml
|
||||
|
||||
|
||||
# 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
|
||||
190
.trellis/scripts/common/developer.py
Executable file
190
.trellis/scripts/common/developer.py
Executable 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: python3 ./{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()
|
||||
31
.trellis/scripts/common/git.py
Executable file
31
.trellis/scripts/common/git.py
Executable 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)
|
||||
78
.trellis/scripts/common/git_context.py
Executable file
78
.trellis/scripts/common/git_context.py
Executable file
@ -0,0 +1,78 @@
|
||||
#!/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,
|
||||
)
|
||||
|
||||
# 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"],
|
||||
default="default",
|
||||
help="Output mode: default (full context), record (for record-session), packages (package info only)",
|
||||
)
|
||||
|
||||
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())
|
||||
else:
|
||||
if args.json:
|
||||
output_json()
|
||||
else:
|
||||
output_text()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
37
.trellis/scripts/common/io.py
Executable file
37
.trellis/scripts/common/io.py
Executable 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
|
||||
45
.trellis/scripts/common/log.py
Executable file
45
.trellis/scripts/common/log.py
Executable 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}")
|
||||
238
.trellis/scripts/common/packages_context.py
Executable file
238
.trellis/scripts/common/packages_context.py
Executable 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,
|
||||
}
|
||||
444
.trellis/scripts/common/paths.py
Executable file
444
.trellis/scripts/common/paths.py
Executable file
@ -0,0 +1,444 @@
|
||||
#!/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 _get_current_task_file(repo_root: Path | None = None) -> Path:
|
||||
"""Get .current-task file path.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
Path to .current-task file.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = get_repo_root()
|
||||
return repo_root / DIR_WORKFLOW / FILE_CURRENT_TASK
|
||||
|
||||
|
||||
def normalize_task_ref(task_ref: str) -> str:
|
||||
"""Normalize a task ref for stable storage in .current-task.
|
||||
|
||||
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 from .current-task 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) -> 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.
|
||||
"""
|
||||
current_file = _get_current_task_file(repo_root)
|
||||
|
||||
if not current_file.is_file():
|
||||
return None
|
||||
|
||||
try:
|
||||
content = current_file.read_text(encoding="utf-8").strip()
|
||||
return normalize_task_ref(content) if content else None
|
||||
except (OSError, IOError):
|
||||
return None
|
||||
|
||||
|
||||
def get_current_task_abs(repo_root: Path | 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)
|
||||
if relative:
|
||||
return resolve_task_ref(relative, repo_root)
|
||||
return None
|
||||
|
||||
|
||||
def set_current_task(task_path: str, repo_root: Path | None = None) -> bool:
|
||||
"""Set current task.
|
||||
|
||||
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()
|
||||
|
||||
normalized = normalize_task_ref(task_path)
|
||||
if not normalized:
|
||||
return False
|
||||
|
||||
# Verify task directory exists
|
||||
full_path = resolve_task_ref(normalized, repo_root)
|
||||
if full_path is None or not full_path.is_dir():
|
||||
return False
|
||||
|
||||
try:
|
||||
normalized = full_path.relative_to(repo_root).as_posix()
|
||||
except ValueError:
|
||||
normalized = str(full_path)
|
||||
|
||||
current_file = _get_current_task_file(repo_root)
|
||||
|
||||
try:
|
||||
current_file.write_text(normalized, encoding="utf-8")
|
||||
return True
|
||||
except (OSError, IOError):
|
||||
return False
|
||||
|
||||
|
||||
def clear_current_task(repo_root: Path | None = None) -> bool:
|
||||
"""Clear current task.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
True on success.
|
||||
"""
|
||||
current_file = _get_current_task_file(repo_root)
|
||||
|
||||
try:
|
||||
if current_file.is_file():
|
||||
current_file.unlink()
|
||||
return True
|
||||
except (OSError, IOError):
|
||||
return False
|
||||
|
||||
|
||||
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)}")
|
||||
254
.trellis/scripts/common/phase.py
Executable file
254
.trellis/scripts/common/phase.py
Executable file
@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Phase Management Utilities.
|
||||
|
||||
Centralized phase tracking for multi-agent pipeline.
|
||||
|
||||
Provides:
|
||||
get_current_phase - Returns current phase number
|
||||
get_total_phases - Returns total phase count
|
||||
get_phase_action - Returns action name for phase
|
||||
get_phase_info - Returns "N/M (action)" format
|
||||
set_phase - Sets current_phase
|
||||
advance_phase - Advances to next phase
|
||||
get_phase_for_action - Returns phase number for action
|
||||
map_subagent_to_action - Map subagent type to action name
|
||||
is_phase_completed - Check if phase is completed
|
||||
is_current_action - Check if at specific action
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .io import read_json, write_json
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Internal Helpers (operate on pre-loaded data dict)
|
||||
# =============================================================================
|
||||
|
||||
def _total_phases(data: dict) -> int:
|
||||
"""Get total phases from pre-loaded data."""
|
||||
next_action = data.get("next_action", [])
|
||||
return len(next_action) if isinstance(next_action, list) else 0
|
||||
|
||||
|
||||
def _phase_action(data: dict, phase: int) -> str:
|
||||
"""Get action name for a phase from pre-loaded data."""
|
||||
next_action = data.get("next_action", [])
|
||||
if isinstance(next_action, list):
|
||||
for item in next_action:
|
||||
if isinstance(item, dict) and item.get("phase") == phase:
|
||||
return item.get("action", "unknown")
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _phase_for_action(data: dict, action: str) -> int:
|
||||
"""Get phase number for an action name from pre-loaded data."""
|
||||
next_action = data.get("next_action", [])
|
||||
if isinstance(next_action, list):
|
||||
for item in next_action:
|
||||
if isinstance(item, dict) and item.get("action") == action:
|
||||
return item.get("phase", 0)
|
||||
return 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Phase Functions
|
||||
# =============================================================================
|
||||
|
||||
def get_current_phase(task_json: Path) -> int:
|
||||
"""Get current phase number.
|
||||
|
||||
Args:
|
||||
task_json: Path to task.json file.
|
||||
|
||||
Returns:
|
||||
Current phase number, or 0 if not found.
|
||||
"""
|
||||
data = read_json(task_json)
|
||||
if not data:
|
||||
return 0
|
||||
return data.get("current_phase", 0) or 0
|
||||
|
||||
|
||||
def get_total_phases(task_json: Path) -> int:
|
||||
"""Get total number of phases.
|
||||
|
||||
Args:
|
||||
task_json: Path to task.json file.
|
||||
|
||||
Returns:
|
||||
Total phase count, or 0 if not found.
|
||||
"""
|
||||
data = read_json(task_json)
|
||||
if not data:
|
||||
return 0
|
||||
return _total_phases(data)
|
||||
|
||||
|
||||
def get_phase_action(task_json: Path, phase: int) -> str:
|
||||
"""Get action name for a specific phase.
|
||||
|
||||
Args:
|
||||
task_json: Path to task.json file.
|
||||
phase: Phase number.
|
||||
|
||||
Returns:
|
||||
Action name, or "unknown" if not found.
|
||||
"""
|
||||
data = read_json(task_json)
|
||||
if not data:
|
||||
return "unknown"
|
||||
return _phase_action(data, phase)
|
||||
|
||||
|
||||
def get_phase_info(task_json: Path) -> str:
|
||||
"""Get formatted phase info: "N/M (action)".
|
||||
|
||||
Args:
|
||||
task_json: Path to task.json file.
|
||||
|
||||
Returns:
|
||||
Formatted string like "1/4 (implement)".
|
||||
"""
|
||||
data = read_json(task_json)
|
||||
if not data:
|
||||
return "N/A"
|
||||
|
||||
current_phase = data.get("current_phase", 0) or 0
|
||||
total = _total_phases(data)
|
||||
action_name = _phase_action(data, current_phase)
|
||||
|
||||
if current_phase == 0 or current_phase is None:
|
||||
return f"0/{total} (pending)"
|
||||
else:
|
||||
return f"{current_phase}/{total} ({action_name})"
|
||||
|
||||
|
||||
def set_phase(task_json: Path, phase: int) -> bool:
|
||||
"""Set current phase to a specific value.
|
||||
|
||||
Args:
|
||||
task_json: Path to task.json file.
|
||||
phase: Phase number to set.
|
||||
|
||||
Returns:
|
||||
True on success, False on error.
|
||||
"""
|
||||
data = read_json(task_json)
|
||||
if not data:
|
||||
return False
|
||||
|
||||
data["current_phase"] = phase
|
||||
return write_json(task_json, data)
|
||||
|
||||
|
||||
def advance_phase(task_json: Path) -> bool:
|
||||
"""Advance to next phase.
|
||||
|
||||
Args:
|
||||
task_json: Path to task.json file.
|
||||
|
||||
Returns:
|
||||
True on success, False on error or at final phase.
|
||||
"""
|
||||
data = read_json(task_json)
|
||||
if not data:
|
||||
return False
|
||||
|
||||
current = data.get("current_phase", 0) or 0
|
||||
total = _total_phases(data)
|
||||
next_phase = current + 1
|
||||
|
||||
if next_phase > total:
|
||||
return False # Already at final phase
|
||||
|
||||
data["current_phase"] = next_phase
|
||||
return write_json(task_json, data)
|
||||
|
||||
|
||||
def get_phase_for_action(task_json: Path, action: str) -> int:
|
||||
"""Get phase number for a specific action name.
|
||||
|
||||
Args:
|
||||
task_json: Path to task.json file.
|
||||
action: Action name.
|
||||
|
||||
Returns:
|
||||
Phase number, or 0 if not found.
|
||||
"""
|
||||
data = read_json(task_json)
|
||||
if not data:
|
||||
return 0
|
||||
return _phase_for_action(data, action)
|
||||
|
||||
|
||||
def map_subagent_to_action(subagent_type: str) -> str:
|
||||
"""Map subagent type to action name.
|
||||
|
||||
Used by hooks to determine which action a subagent corresponds to.
|
||||
|
||||
Args:
|
||||
subagent_type: Subagent type string.
|
||||
|
||||
Returns:
|
||||
Corresponding action name.
|
||||
"""
|
||||
mapping = {
|
||||
"implement": "implement",
|
||||
"check": "check",
|
||||
"debug": "debug",
|
||||
"research": "research",
|
||||
}
|
||||
return mapping.get(subagent_type, subagent_type)
|
||||
|
||||
|
||||
def is_phase_completed(task_json: Path, phase: int) -> bool:
|
||||
"""Check if a phase is completed (current_phase > phase).
|
||||
|
||||
Args:
|
||||
task_json: Path to task.json file.
|
||||
phase: Phase number to check.
|
||||
|
||||
Returns:
|
||||
True if phase is completed.
|
||||
"""
|
||||
current = get_current_phase(task_json)
|
||||
return current > phase
|
||||
|
||||
|
||||
def is_current_action(task_json: Path, action: str) -> bool:
|
||||
"""Check if we're at a specific action.
|
||||
|
||||
Args:
|
||||
task_json: Path to task.json file.
|
||||
action: Action name to check.
|
||||
|
||||
Returns:
|
||||
True if current phase matches the action.
|
||||
"""
|
||||
data = read_json(task_json)
|
||||
if not data:
|
||||
return False
|
||||
current = data.get("current_phase", 0) or 0
|
||||
action_phase = _phase_for_action(data, action)
|
||||
return current == action_phase
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main Entry (for testing)
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
path = Path(sys.argv[1])
|
||||
print(f"Task JSON: {path}")
|
||||
print(f"Phase info: {get_phase_info(path)}")
|
||||
print(f"Current phase: {get_current_phase(path)}")
|
||||
print(f"Total phases: {get_total_phases(path)}")
|
||||
else:
|
||||
print("Usage: python3 phase.py <task.json>")
|
||||
335
.trellis/scripts/common/registry.py
Executable file
335
.trellis/scripts/common/registry.py
Executable file
@ -0,0 +1,335 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Registry utility functions for multi-agent pipeline.
|
||||
|
||||
Provides:
|
||||
registry_get_file - Get registry file path
|
||||
registry_get_agent_by_id - Find agent by ID
|
||||
registry_get_agent_by_worktree - Find agent by worktree path
|
||||
registry_get_task_dir - Get task dir for a worktree
|
||||
registry_remove_by_id - Remove agent by ID
|
||||
registry_remove_by_worktree - Remove agent by worktree path
|
||||
registry_add_agent - Add agent to registry
|
||||
registry_search_agent - Search agent by ID or task_dir
|
||||
registry_list_agents - List all agents
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from .io import read_json, write_json
|
||||
from .paths import get_repo_root
|
||||
from .worktree import get_agents_dir
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Internal Helpers
|
||||
# =============================================================================
|
||||
|
||||
def _load_registry(
|
||||
repo_root: Path | None = None,
|
||||
) -> tuple[Path | None, dict | None]:
|
||||
"""Load registry file and data in one step.
|
||||
|
||||
Returns:
|
||||
(registry_file_path, data_dict) — either may be None.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = get_repo_root()
|
||||
|
||||
registry_file = registry_get_file(repo_root)
|
||||
if not registry_file or not registry_file.is_file():
|
||||
return registry_file, None
|
||||
|
||||
data = read_json(registry_file)
|
||||
return registry_file, data
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Registry File Access
|
||||
# =============================================================================
|
||||
|
||||
def registry_get_file(repo_root: Path | None = None) -> Path | None:
|
||||
"""Get registry file path.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
Path to registry.json, or None if agents dir not found.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = get_repo_root()
|
||||
|
||||
agents_dir = get_agents_dir(repo_root)
|
||||
if agents_dir:
|
||||
return agents_dir / "registry.json"
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_registry(repo_root: Path | None = None) -> Path | None:
|
||||
"""Ensure registry file exists with valid structure.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
Path to registry file, or None if cannot create.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = get_repo_root()
|
||||
|
||||
registry_file = registry_get_file(repo_root)
|
||||
if not registry_file:
|
||||
return None
|
||||
|
||||
agents_dir = registry_file.parent
|
||||
|
||||
try:
|
||||
agents_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not registry_file.exists():
|
||||
write_json(registry_file, {"agents": []})
|
||||
|
||||
return registry_file
|
||||
except (OSError, IOError):
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent Lookup
|
||||
# =============================================================================
|
||||
|
||||
def registry_get_agent_by_id(
|
||||
agent_id: str,
|
||||
repo_root: Path | None = None
|
||||
) -> dict | None:
|
||||
"""Get agent by ID.
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID.
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
Agent dict, or None if not found.
|
||||
"""
|
||||
_, data = _load_registry(repo_root)
|
||||
if not data:
|
||||
return None
|
||||
|
||||
for agent in data.get("agents", []):
|
||||
if agent.get("id") == agent_id:
|
||||
return agent
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def registry_get_agent_by_worktree(
|
||||
worktree_path: str,
|
||||
repo_root: Path | None = None
|
||||
) -> dict | None:
|
||||
"""Get agent by worktree path.
|
||||
|
||||
Args:
|
||||
worktree_path: Worktree path.
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
Agent dict, or None if not found.
|
||||
"""
|
||||
_, data = _load_registry(repo_root)
|
||||
if not data:
|
||||
return None
|
||||
|
||||
for agent in data.get("agents", []):
|
||||
if agent.get("worktree_path") == worktree_path:
|
||||
return agent
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def registry_search_agent(
|
||||
search: str,
|
||||
repo_root: Path | None = None
|
||||
) -> dict | None:
|
||||
"""Search agent by ID or task_dir containing search term.
|
||||
|
||||
Args:
|
||||
search: Search term.
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
First matching agent dict, or None if not found.
|
||||
"""
|
||||
_, data = _load_registry(repo_root)
|
||||
if not data:
|
||||
return None
|
||||
|
||||
for agent in data.get("agents", []):
|
||||
# Exact ID match
|
||||
if agent.get("id") == search:
|
||||
return agent
|
||||
# Partial match on task_dir
|
||||
task_dir = agent.get("task_dir", "")
|
||||
if search in task_dir:
|
||||
return agent
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def registry_get_task_dir(
|
||||
worktree_path: str,
|
||||
repo_root: Path | None = None
|
||||
) -> str | None:
|
||||
"""Get task directory for a worktree.
|
||||
|
||||
Args:
|
||||
worktree_path: Worktree path.
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
Task directory path, or None if not found.
|
||||
"""
|
||||
_, data = _load_registry(repo_root)
|
||||
if not data:
|
||||
return None
|
||||
|
||||
for agent in data.get("agents", []):
|
||||
if agent.get("worktree_path") == worktree_path:
|
||||
return agent.get("task_dir")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent Modification
|
||||
# =============================================================================
|
||||
|
||||
def registry_remove_by_id(agent_id: str, repo_root: Path | None = None) -> bool:
|
||||
"""Remove agent by ID.
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID.
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
True on success.
|
||||
"""
|
||||
registry_file, data = _load_registry(repo_root)
|
||||
if not registry_file or not data:
|
||||
return True # Nothing to remove
|
||||
|
||||
agents = data.get("agents", [])
|
||||
data["agents"] = [a for a in agents if a.get("id") != agent_id]
|
||||
|
||||
return write_json(registry_file, data)
|
||||
|
||||
|
||||
def registry_remove_by_worktree(
|
||||
worktree_path: str,
|
||||
repo_root: Path | None = None
|
||||
) -> bool:
|
||||
"""Remove agent by worktree path.
|
||||
|
||||
Args:
|
||||
worktree_path: Worktree path.
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
True on success.
|
||||
"""
|
||||
registry_file, data = _load_registry(repo_root)
|
||||
if not registry_file or not data:
|
||||
return True # Nothing to remove
|
||||
|
||||
agents = data.get("agents", [])
|
||||
data["agents"] = [a for a in agents if a.get("worktree_path") != worktree_path]
|
||||
|
||||
return write_json(registry_file, data)
|
||||
|
||||
|
||||
def registry_add_agent(
|
||||
agent_id: str,
|
||||
worktree_path: str,
|
||||
pid: int,
|
||||
task_dir: str,
|
||||
repo_root: Path | None = None,
|
||||
platform: str = "claude",
|
||||
) -> bool:
|
||||
"""Add agent to registry (replaces if same ID exists).
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID.
|
||||
worktree_path: Worktree path.
|
||||
pid: Process ID.
|
||||
task_dir: Task directory path.
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
platform: Platform used (e.g., 'claude', 'opencode', 'codex', 'kiro', 'antigravity'). Defaults to 'claude'.
|
||||
|
||||
Returns:
|
||||
True on success.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = get_repo_root()
|
||||
|
||||
registry_file = _ensure_registry(repo_root)
|
||||
if not registry_file:
|
||||
return False
|
||||
|
||||
data = read_json(registry_file)
|
||||
if not data:
|
||||
data = {"agents": []}
|
||||
|
||||
# Remove existing agent with same ID
|
||||
agents = data.get("agents", [])
|
||||
agents = [a for a in agents if a.get("id") != agent_id]
|
||||
|
||||
# Create new agent record
|
||||
started_at = datetime.now().isoformat()
|
||||
new_agent = {
|
||||
"id": agent_id,
|
||||
"worktree_path": worktree_path,
|
||||
"pid": pid,
|
||||
"started_at": started_at,
|
||||
"task_dir": task_dir,
|
||||
"platform": platform,
|
||||
}
|
||||
|
||||
agents.append(new_agent)
|
||||
data["agents"] = agents
|
||||
|
||||
return write_json(registry_file, data)
|
||||
|
||||
|
||||
def registry_list_agents(repo_root: Path | None = None) -> list[dict]:
|
||||
"""List all agents.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
List of agent dicts.
|
||||
"""
|
||||
_, data = _load_registry(repo_root)
|
||||
if not data:
|
||||
return []
|
||||
|
||||
return data.get("agents", [])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main Entry (for testing)
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json as json_mod
|
||||
|
||||
repo = get_repo_root()
|
||||
print(f"Repository root: {repo}")
|
||||
print(f"Registry file: {registry_get_file(repo)}")
|
||||
print()
|
||||
print("Agents:")
|
||||
agents = registry_list_agents(repo)
|
||||
print(json_mod.dumps(agents, indent=2))
|
||||
562
.trellis/scripts/common/session_context.py
Executable file
562
.trellis/scripts/common/session_context.py
Executable file
@ -0,0 +1,562 @@
|
||||
#!/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_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: python3 ./{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
|
||||
lines.append(f"Path: {current_task}")
|
||||
|
||||
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:
|
||||
ct = load_task(repo_root / current_task)
|
||||
if ct:
|
||||
current_task_info = {
|
||||
"path": current_task,
|
||||
"name": ct.name,
|
||||
"status": ct.status,
|
||||
}
|
||||
|
||||
# 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: python3 ./{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:
|
||||
lines.append(f"Path: {current_task}")
|
||||
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))
|
||||
410
.trellis/scripts/common/task_context.py
Executable file
410
.trellis/scripts/common/task_context.py
Executable file
@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Task JSONL context management.
|
||||
|
||||
Provides:
|
||||
cmd_init_context - Initialize JSONL context files for a task
|
||||
cmd_add_context - Add entry to JSONL context file
|
||||
cmd_validate - Validate JSONL context files
|
||||
cmd_list_context - List JSONL context entries
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from .cli_adapter import get_cli_adapter_auto
|
||||
from .config import (
|
||||
get_packages,
|
||||
is_monorepo,
|
||||
resolve_package,
|
||||
validate_package,
|
||||
)
|
||||
from .io import read_json, write_json
|
||||
from .log import Colors, colored
|
||||
from .paths import (
|
||||
DIR_SPEC,
|
||||
DIR_WORKFLOW,
|
||||
FILE_TASK_JSON,
|
||||
get_repo_root,
|
||||
)
|
||||
from .task_utils import resolve_task_dir
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# JSONL Default Content Generators
|
||||
# =============================================================================
|
||||
|
||||
def get_implement_base() -> list[dict]:
|
||||
"""Get base implement context entries."""
|
||||
return [
|
||||
{"file": f"{DIR_WORKFLOW}/workflow.md", "reason": "Project workflow and conventions"},
|
||||
]
|
||||
|
||||
|
||||
def get_implement_backend(package: str | None = None) -> list[dict]:
|
||||
"""Get backend implement context entries."""
|
||||
spec_base = f"{DIR_SPEC}/{package}" if package else DIR_SPEC
|
||||
return [
|
||||
{"file": f"{DIR_WORKFLOW}/{spec_base}/backend/index.md", "reason": "Backend development guide"},
|
||||
]
|
||||
|
||||
|
||||
def get_implement_frontend(package: str | None = None) -> list[dict]:
|
||||
"""Get frontend implement context entries."""
|
||||
spec_base = f"{DIR_SPEC}/{package}" if package else DIR_SPEC
|
||||
return [
|
||||
{"file": f"{DIR_WORKFLOW}/{spec_base}/frontend/index.md", "reason": "Frontend development guide"},
|
||||
]
|
||||
|
||||
|
||||
def get_check_context(repo_root: Path) -> list[dict]:
|
||||
"""Get check context entries."""
|
||||
adapter = get_cli_adapter_auto(repo_root)
|
||||
|
||||
entries = [
|
||||
{"file": adapter.get_trellis_command_path("finish-work"), "reason": "Finish work checklist"},
|
||||
{"file": adapter.get_trellis_command_path("check"), "reason": "Code quality check spec"},
|
||||
]
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def get_debug_context(repo_root: Path) -> list[dict]:
|
||||
"""Get debug context entries."""
|
||||
adapter = get_cli_adapter_auto(repo_root)
|
||||
|
||||
entries: list[dict] = [
|
||||
{"file": adapter.get_trellis_command_path("check"), "reason": "Code quality check spec"},
|
||||
]
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, entries: list[dict]) -> None:
|
||||
"""Write entries to JSONL file."""
|
||||
lines = [json.dumps(entry, ensure_ascii=False) for entry in entries]
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Command: init-context
|
||||
# =============================================================================
|
||||
|
||||
def cmd_init_context(args: argparse.Namespace) -> int:
|
||||
"""Initialize JSONL context files for a task."""
|
||||
repo_root = get_repo_root()
|
||||
target_dir = resolve_task_dir(args.dir, repo_root)
|
||||
dev_type = args.type
|
||||
|
||||
if not dev_type:
|
||||
print(colored("Error: Missing arguments", Colors.RED))
|
||||
print("Usage: python3 task.py init-context <task-dir> <dev_type>")
|
||||
print(" dev_type: backend | frontend | fullstack | test | docs")
|
||||
return 1
|
||||
|
||||
if not target_dir.is_dir():
|
||||
print(colored(f"Error: Directory not found: {target_dir}", Colors.RED))
|
||||
return 1
|
||||
|
||||
# Resolve package: --package CLI → task.json.package → default_package
|
||||
cli_package: str | None = getattr(args, "package", None)
|
||||
package: str | None = None
|
||||
if not is_monorepo(repo_root):
|
||||
# Single-repo: ignore --package, no package prefix
|
||||
if cli_package:
|
||||
print(colored("Warning: --package ignored in single-repo project", Colors.YELLOW), file=sys.stderr)
|
||||
elif cli_package:
|
||||
if not validate_package(cli_package, repo_root):
|
||||
packages = get_packages(repo_root)
|
||||
available = ", ".join(sorted(packages.keys())) if packages else "(none)"
|
||||
print(colored(f"Error: unknown package '{cli_package}'. Available: {available}", Colors.RED), file=sys.stderr)
|
||||
return 1
|
||||
package = cli_package
|
||||
else:
|
||||
# Read task.json.package as inferred source
|
||||
task_json_path = target_dir / FILE_TASK_JSON
|
||||
task_pkg_value = None
|
||||
if task_json_path.is_file():
|
||||
task_data = read_json(task_json_path)
|
||||
if isinstance(task_data, dict):
|
||||
task_pkg_value = task_data.get("package")
|
||||
# Only pass string values to resolve_package (guard against malformed JSON)
|
||||
task_package = task_pkg_value if isinstance(task_pkg_value, str) else None
|
||||
package = resolve_package(task_package=task_package, repo_root=repo_root)
|
||||
|
||||
# Monorepo fallback prohibition
|
||||
if package is None:
|
||||
packages = get_packages(repo_root)
|
||||
available = ", ".join(sorted(packages.keys())) if packages else "(none)"
|
||||
print(colored(
|
||||
f"Error: monorepo project requires --package (or set default_package in config.yaml). Available: {available}",
|
||||
Colors.RED,
|
||||
), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(colored("=== Initializing Agent Context Files ===", Colors.BLUE))
|
||||
print(f"Target dir: {target_dir}")
|
||||
print(f"Dev type: {dev_type}")
|
||||
if package:
|
||||
print(f"Package: {package}")
|
||||
print()
|
||||
|
||||
# implement.jsonl
|
||||
print(colored("Creating implement.jsonl...", Colors.CYAN))
|
||||
implement_entries = get_implement_base()
|
||||
if dev_type in ("backend", "test"):
|
||||
implement_entries.extend(get_implement_backend(package))
|
||||
elif dev_type == "frontend":
|
||||
implement_entries.extend(get_implement_frontend(package))
|
||||
elif dev_type == "fullstack":
|
||||
implement_entries.extend(get_implement_backend(package))
|
||||
implement_entries.extend(get_implement_frontend(package))
|
||||
|
||||
implement_file = target_dir / "implement.jsonl"
|
||||
_write_jsonl(implement_file, implement_entries)
|
||||
print(f" {colored('✓', Colors.GREEN)} {len(implement_entries)} entries")
|
||||
|
||||
# check.jsonl
|
||||
print(colored("Creating check.jsonl...", Colors.CYAN))
|
||||
check_entries = get_check_context(repo_root)
|
||||
check_file = target_dir / "check.jsonl"
|
||||
_write_jsonl(check_file, check_entries)
|
||||
print(f" {colored('✓', Colors.GREEN)} {len(check_entries)} entries")
|
||||
|
||||
# debug.jsonl
|
||||
print(colored("Creating debug.jsonl...", Colors.CYAN))
|
||||
debug_entries = get_debug_context(repo_root)
|
||||
debug_file = target_dir / "debug.jsonl"
|
||||
_write_jsonl(debug_file, debug_entries)
|
||||
print(f" {colored('✓', Colors.GREEN)} {len(debug_entries)} entries")
|
||||
|
||||
# Update task.json dev_type and package
|
||||
task_json_path = target_dir / FILE_TASK_JSON
|
||||
if task_json_path.is_file():
|
||||
task_data = read_json(task_json_path)
|
||||
if isinstance(task_data, dict):
|
||||
task_data["dev_type"] = dev_type
|
||||
task_data["package"] = package # Always sync to match resolved value
|
||||
write_json(task_json_path, task_data)
|
||||
|
||||
print()
|
||||
print(colored("✓ All context files created", Colors.GREEN))
|
||||
print()
|
||||
|
||||
# Show what was auto-injected
|
||||
all_injected = [e["file"] for e in implement_entries]
|
||||
print(colored("Auto-injected (defaults only):", Colors.YELLOW))
|
||||
for f in all_injected:
|
||||
print(f" - {f}")
|
||||
print()
|
||||
|
||||
# Scan spec directory for available spec files the AI should consider
|
||||
spec_base = repo_root / DIR_WORKFLOW / DIR_SPEC
|
||||
if package:
|
||||
spec_base = spec_base / package
|
||||
available_specs: list[str] = []
|
||||
if spec_base.is_dir():
|
||||
for md_file in sorted(spec_base.rglob("*.md")):
|
||||
rel = str(md_file.relative_to(repo_root))
|
||||
if rel not in all_injected:
|
||||
available_specs.append(rel)
|
||||
|
||||
if available_specs:
|
||||
print(colored("Available spec files (not yet injected):", Colors.BLUE))
|
||||
for spec in available_specs:
|
||||
print(f" - {spec}")
|
||||
print()
|
||||
|
||||
print(colored("Next steps:", Colors.BLUE))
|
||||
print(" 1. Review the spec files above and add relevant ones for your task:")
|
||||
print(f" python3 task.py add-context <dir> implement <spec-path> \"<reason>\"")
|
||||
print(" 2. Set as current: python3 task.py start <dir>")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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", "debug.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."""
|
||||
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
|
||||
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:
|
||||
print(f" {colored(f'{file_name}:{line_num}: Missing file field', Colors.RED)}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
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}: ✓ ({line_num} 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", "debug.jsonl"]:
|
||||
jsonl_file = target_dir / jsonl_name
|
||||
if not jsonl_file.is_file():
|
||||
continue
|
||||
|
||||
print(colored(f"[{jsonl_name}]", Colors.CYAN))
|
||||
|
||||
count = 0
|
||||
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
|
||||
|
||||
count += 1
|
||||
file_path = data.get("file", "?")
|
||||
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}")
|
||||
|
||||
print()
|
||||
|
||||
return 0
|
||||
188
.trellis/scripts/common/task_queue.py
Executable file
188
.trellis/scripts/common/task_queue.py
Executable 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']}")
|
||||
536
.trellis/scripts/common/task_store.py
Executable file
536
.trellis/scripts/common/task_store.py
Executable file
@ -0,0 +1,536 @@
|
||||
#!/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 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,
|
||||
clear_current_task,
|
||||
generate_task_date_prefix,
|
||||
get_current_task,
|
||||
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
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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,
|
||||
"current_phase": 0,
|
||||
"next_action": [
|
||||
{"phase": 1, "action": "brainstorm"},
|
||||
{"phase": 2, "action": "research"},
|
||||
{"phase": 3, "action": "implement"},
|
||||
{"phase": 4, "action": "check"},
|
||||
{"phase": 5, "action": "update-spec"},
|
||||
{"phase": 6, "action": "record-session"},
|
||||
],
|
||||
"commit": None,
|
||||
"pr_url": None,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": None,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {},
|
||||
}
|
||||
|
||||
write_json(task_json_path, task_data)
|
||||
|
||||
# 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)
|
||||
|
||||
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)
|
||||
print(" 2. Run: python3 task.py init-context <dir> <dev_type>", file=sys.stderr)
|
||||
print(" 3. Run: python3 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)
|
||||
|
||||
# Find task directory
|
||||
task_dir = find_task_by_name(task_name, tasks_dir)
|
||||
|
||||
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
|
||||
task_parent = data.get("parent")
|
||||
task_children = data.get("children", [])
|
||||
|
||||
# If this is a child, remove from parent's children list
|
||||
if task_parent:
|
||||
parent_dir = find_task_by_name(task_parent, tasks_dir)
|
||||
if parent_dir:
|
||||
parent_json = parent_dir / FILE_TASK_JSON
|
||||
if parent_json.is_file():
|
||||
parent_data = read_json(parent_json)
|
||||
if parent_data:
|
||||
parent_children = parent_data.get("children", [])
|
||||
if dir_name in parent_children:
|
||||
parent_children.remove(dir_name)
|
||||
parent_data["children"] = parent_children
|
||||
write_json(parent_json, parent_data)
|
||||
|
||||
# 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 if current task
|
||||
current = get_current_task(repo_root)
|
||||
if current and dir_name in current:
|
||||
clear_current_task(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: python3 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))
|
||||
print()
|
||||
print(colored("Now you can start the multi-agent pipeline:", Colors.BLUE))
|
||||
print(f" python3 ./.trellis/scripts/multi_agent/start.py {args.dir}")
|
||||
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: python3 task.py set-base-branch <task-dir> <base-branch>")
|
||||
print("Example: python3 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: python3 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
|
||||
274
.trellis/scripts/common/task_utils.py
Executable file
274
.trellis/scripts/common/task_utils.py
Executable 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)}")
|
||||
109
.trellis/scripts/common/tasks.py
Executable file
109
.trellis/scripts/common/tasks.py
Executable file
@ -0,0 +1,109 @@
|
||||
"""
|
||||
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 ""
|
||||
done = sum(
|
||||
1 for c in children
|
||||
if all_statuses.get(c) in ("completed", "done")
|
||||
)
|
||||
return f" [{done}/{len(children)} done]"
|
||||
112
.trellis/scripts/common/types.py
Executable file
112
.trellis/scripts/common/types.py
Executable file
@ -0,0 +1,112 @@
|
||||
"""
|
||||
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
|
||||
current_phase: int
|
||||
next_action: list[dict]
|
||||
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
|
||||
305
.trellis/scripts/common/worktree.py
Executable file
305
.trellis/scripts/common/worktree.py
Executable file
@ -0,0 +1,305 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Worktree utilities for Multi-Agent Pipeline.
|
||||
|
||||
Provides:
|
||||
get_worktree_config - Get worktree.yaml path
|
||||
get_worktree_base_dir - Get worktree storage directory
|
||||
get_worktree_copy_files - Get files to copy list
|
||||
get_worktree_post_create_hooks - Get post-create hooks
|
||||
get_agents_dir - Get agents registry directory
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .paths import (
|
||||
DIR_WORKFLOW,
|
||||
get_repo_root,
|
||||
get_workspace_dir,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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, ""
|
||||
|
||||
|
||||
def _yaml_get_value(config_file: Path, key: str) -> str | None:
|
||||
"""Read simple value from worktree.yaml.
|
||||
|
||||
Args:
|
||||
config_file: Path to config file.
|
||||
key: Key to read.
|
||||
|
||||
Returns:
|
||||
Value string or None.
|
||||
"""
|
||||
try:
|
||||
content = config_file.read_text(encoding="utf-8")
|
||||
data = parse_simple_yaml(content)
|
||||
value = data.get(key)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _yaml_get_list(config_file: Path, section: str) -> list[str]:
|
||||
"""Read list from worktree.yaml.
|
||||
|
||||
Args:
|
||||
config_file: Path to config file.
|
||||
section: Section name.
|
||||
|
||||
Returns:
|
||||
List of items.
|
||||
"""
|
||||
try:
|
||||
content = config_file.read_text(encoding="utf-8")
|
||||
data = parse_simple_yaml(content)
|
||||
value = data.get(section)
|
||||
if isinstance(value, list):
|
||||
return [str(item) for item in value]
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Worktree Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Worktree config file relative path (relative to repo root)
|
||||
WORKTREE_CONFIG_PATH = f"{DIR_WORKFLOW}/worktree.yaml"
|
||||
|
||||
|
||||
def get_worktree_config(repo_root: Path | None = None) -> Path:
|
||||
"""Get worktree.yaml config file path.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
Absolute path to config file.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = get_repo_root()
|
||||
return repo_root / WORKTREE_CONFIG_PATH
|
||||
|
||||
|
||||
def get_worktree_base_dir(repo_root: Path | None = None) -> Path:
|
||||
"""Get worktree base directory.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
Absolute path to worktree base directory.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = get_repo_root()
|
||||
|
||||
config = get_worktree_config(repo_root)
|
||||
worktree_dir = _yaml_get_value(config, "worktree_dir")
|
||||
|
||||
# Default value
|
||||
if not worktree_dir:
|
||||
worktree_dir = "../worktrees"
|
||||
|
||||
# Handle relative path
|
||||
if worktree_dir.startswith("../") or worktree_dir.startswith("./"):
|
||||
# Relative to repo_root
|
||||
return repo_root / worktree_dir
|
||||
else:
|
||||
# Absolute path
|
||||
return Path(worktree_dir)
|
||||
|
||||
|
||||
def get_worktree_copy_files(repo_root: Path | None = None) -> list[str]:
|
||||
"""Get files to copy list.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
List of file paths to copy.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = get_repo_root()
|
||||
config = get_worktree_config(repo_root)
|
||||
return _yaml_get_list(config, "copy")
|
||||
|
||||
|
||||
def get_worktree_post_create_hooks(repo_root: Path | None = None) -> list[str]:
|
||||
"""Get post_create hooks.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
List of commands to run.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = get_repo_root()
|
||||
config = get_worktree_config(repo_root)
|
||||
return _yaml_get_list(config, "post_create")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agents Registry
|
||||
# =============================================================================
|
||||
|
||||
def get_agents_dir(repo_root: Path | None = None) -> Path | None:
|
||||
"""Get agents directory for current developer.
|
||||
|
||||
Args:
|
||||
repo_root: Repository root path. Defaults to auto-detected.
|
||||
|
||||
Returns:
|
||||
Absolute path to agents directory, or None if no workspace.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = get_repo_root()
|
||||
|
||||
workspace_dir = get_workspace_dir(repo_root)
|
||||
if workspace_dir:
|
||||
return workspace_dir / ".agents"
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main Entry (for testing)
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
repo = get_repo_root()
|
||||
print(f"Repository root: {repo}")
|
||||
print(f"Worktree config: {get_worktree_config(repo)}")
|
||||
print(f"Worktree base dir: {get_worktree_base_dir(repo)}")
|
||||
print(f"Copy files: {get_worktree_copy_files(repo)}")
|
||||
print(f"Post create hooks: {get_worktree_post_create_hooks(repo)}")
|
||||
print(f"Agents dir: {get_agents_dir(repo)}")
|
||||
298
.trellis/scripts/create_bootstrap.py
Executable file
298
.trellis/scripts/create_bootstrap.py
Executable file
@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create Bootstrap Task for First-Time Setup.
|
||||
|
||||
Creates a guided task to help users fill in project guidelines
|
||||
after initializing Trellis for the first time.
|
||||
|
||||
Usage:
|
||||
python3 create_bootstrap.py [project-type]
|
||||
|
||||
Arguments:
|
||||
project-type: frontend | backend | fullstack (default: fullstack)
|
||||
|
||||
Prerequisites:
|
||||
- .trellis/.developer must exist (run init_developer.py first)
|
||||
|
||||
Creates:
|
||||
.trellis/tasks/00-bootstrap-guidelines/
|
||||
- task.json # Task metadata
|
||||
- prd.md # Task description and guidance
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from common.paths import (
|
||||
DIR_WORKFLOW,
|
||||
DIR_SCRIPTS,
|
||||
DIR_TASKS,
|
||||
get_repo_root,
|
||||
get_developer,
|
||||
get_tasks_dir,
|
||||
set_current_task,
|
||||
)
|
||||
from common.config import get_spec_base, resolve_package
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
TASK_NAME = "00-bootstrap-guidelines"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRD Content
|
||||
# =============================================================================
|
||||
|
||||
def write_prd_header() -> str:
|
||||
"""Write PRD header section."""
|
||||
return """# Bootstrap: Fill Project Development Guidelines
|
||||
|
||||
## Purpose
|
||||
|
||||
Welcome to Trellis! This is your first task.
|
||||
|
||||
AI agents use `.trellis/spec/` to understand YOUR project's coding conventions.
|
||||
**Starting from scratch = AI writes generic code that doesn't match your project style.**
|
||||
|
||||
Filling these guidelines is a one-time setup that pays off for every future AI session.
|
||||
|
||||
---
|
||||
|
||||
## Your Task
|
||||
|
||||
Fill in the guideline files based on your **existing codebase**.
|
||||
"""
|
||||
|
||||
|
||||
def write_prd_backend_section(spec_base: str) -> str:
|
||||
"""Write PRD backend section."""
|
||||
return f"""
|
||||
|
||||
### Backend Guidelines
|
||||
|
||||
| File | What to Document |
|
||||
|------|------------------|
|
||||
| `.trellis/{spec_base}/backend/directory-structure.md` | Where different file types go (routes, services, utils) |
|
||||
| `.trellis/{spec_base}/backend/database-guidelines.md` | ORM, migrations, query patterns, naming conventions |
|
||||
| `.trellis/{spec_base}/backend/error-handling.md` | How errors are caught, logged, and returned |
|
||||
| `.trellis/{spec_base}/backend/logging-guidelines.md` | Log levels, format, what to log |
|
||||
| `.trellis/{spec_base}/backend/quality-guidelines.md` | Code review standards, testing requirements |
|
||||
"""
|
||||
|
||||
|
||||
def write_prd_frontend_section(spec_base: str) -> str:
|
||||
"""Write PRD frontend section."""
|
||||
return f"""
|
||||
|
||||
### Frontend Guidelines
|
||||
|
||||
| File | What to Document |
|
||||
|------|------------------|
|
||||
| `.trellis/{spec_base}/frontend/directory-structure.md` | Component/page/hook organization |
|
||||
| `.trellis/{spec_base}/frontend/component-guidelines.md` | Component patterns, props conventions |
|
||||
| `.trellis/{spec_base}/frontend/hook-guidelines.md` | Custom hook naming, patterns |
|
||||
| `.trellis/{spec_base}/frontend/state-management.md` | State library, patterns, what goes where |
|
||||
| `.trellis/{spec_base}/frontend/type-safety.md` | TypeScript conventions, type organization |
|
||||
| `.trellis/{spec_base}/frontend/quality-guidelines.md` | Linting, testing, accessibility |
|
||||
"""
|
||||
|
||||
|
||||
def write_prd_footer() -> str:
|
||||
"""Write PRD footer section."""
|
||||
return """
|
||||
|
||||
### Thinking Guides (Optional)
|
||||
|
||||
The `.trellis/spec/guides/` directory contains thinking guides that are already
|
||||
filled with general best practices. You can customize them for your project if needed.
|
||||
|
||||
---
|
||||
|
||||
## How to Fill Guidelines
|
||||
|
||||
### Principle: Document Reality, Not Ideals
|
||||
|
||||
Write what your codebase **actually does**, not what you wish it did.
|
||||
AI needs to match existing patterns, not introduce new ones.
|
||||
|
||||
### Steps
|
||||
|
||||
1. **Look at existing code** - Find 2-3 examples of each pattern
|
||||
2. **Document the pattern** - Describe what you see
|
||||
3. **Include file paths** - Reference real files as examples
|
||||
4. **List anti-patterns** - What does your team avoid?
|
||||
|
||||
---
|
||||
|
||||
## Tips for Using AI
|
||||
|
||||
Ask AI to help analyze your codebase:
|
||||
|
||||
- "Look at my codebase and document the patterns you see"
|
||||
- "Analyze my code structure and summarize the conventions"
|
||||
- "Find error handling patterns and document them"
|
||||
|
||||
The AI will read your code and help you document it.
|
||||
|
||||
---
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
- [ ] Guidelines filled for your project type
|
||||
- [ ] At least 2-3 real code examples in each guideline
|
||||
- [ ] Anti-patterns documented
|
||||
|
||||
When done:
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py finish
|
||||
python3 ./.trellis/scripts/task.py archive 00-bootstrap-guidelines
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why This Matters
|
||||
|
||||
After completing this task:
|
||||
|
||||
1. AI will write code that matches your project style
|
||||
2. Relevant `/trellis:before-*-dev` commands will inject real context
|
||||
3. `/trellis:check-*` commands will validate against your actual standards
|
||||
4. Future developers (human or AI) will onboard faster
|
||||
"""
|
||||
|
||||
|
||||
def write_prd(task_dir: Path, project_type: str, spec_base: str) -> None:
|
||||
"""Write prd.md file."""
|
||||
content = write_prd_header()
|
||||
|
||||
if project_type == "frontend":
|
||||
content += write_prd_frontend_section(spec_base)
|
||||
elif project_type == "backend":
|
||||
content += write_prd_backend_section(spec_base)
|
||||
else: # fullstack
|
||||
content += write_prd_backend_section(spec_base)
|
||||
content += write_prd_frontend_section(spec_base)
|
||||
|
||||
content += write_prd_footer()
|
||||
|
||||
prd_file = task_dir / "prd.md"
|
||||
prd_file.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Task JSON
|
||||
# =============================================================================
|
||||
|
||||
def write_task_json(task_dir: Path, developer: str, project_type: str, spec_base: str) -> None:
|
||||
"""Write task.json file."""
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# Generate subtasks and related files based on project type
|
||||
if project_type == "frontend":
|
||||
subtasks = [
|
||||
{"name": "Fill frontend guidelines", "status": "pending"},
|
||||
{"name": "Add code examples", "status": "pending"},
|
||||
]
|
||||
related_files = [f".trellis/{spec_base}/frontend/"]
|
||||
elif project_type == "backend":
|
||||
subtasks = [
|
||||
{"name": "Fill backend guidelines", "status": "pending"},
|
||||
{"name": "Add code examples", "status": "pending"},
|
||||
]
|
||||
related_files = [f".trellis/{spec_base}/backend/"]
|
||||
else: # fullstack
|
||||
subtasks = [
|
||||
{"name": "Fill backend guidelines", "status": "pending"},
|
||||
{"name": "Fill frontend guidelines", "status": "pending"},
|
||||
{"name": "Add code examples", "status": "pending"},
|
||||
]
|
||||
related_files = [f".trellis/{spec_base}/backend/", f".trellis/{spec_base}/frontend/"]
|
||||
|
||||
task_data = {
|
||||
"id": TASK_NAME,
|
||||
"name": "Bootstrap Guidelines",
|
||||
"description": "Fill in project development guidelines for AI agents",
|
||||
"status": "in_progress",
|
||||
"dev_type": "docs",
|
||||
"priority": "P1",
|
||||
"creator": developer,
|
||||
"assignee": developer,
|
||||
"createdAt": today,
|
||||
"completedAt": None,
|
||||
"commit": None,
|
||||
"subtasks": subtasks,
|
||||
"children": [],
|
||||
"parent": None,
|
||||
"relatedFiles": related_files,
|
||||
"notes": f"First-time setup task created by trellis init ({project_type} project)",
|
||||
"meta": {},
|
||||
}
|
||||
|
||||
task_json = task_dir / "task.json"
|
||||
task_json.write_text(json.dumps(task_data, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point."""
|
||||
# Parse project type argument
|
||||
project_type = "fullstack"
|
||||
if len(sys.argv) > 1:
|
||||
project_type = sys.argv[1]
|
||||
|
||||
# Validate project type
|
||||
if project_type not in ("frontend", "backend", "fullstack"):
|
||||
print(f"Unknown project type: {project_type}, defaulting to fullstack")
|
||||
project_type = "fullstack"
|
||||
|
||||
repo_root = get_repo_root()
|
||||
developer = get_developer(repo_root)
|
||||
|
||||
# Check developer initialized
|
||||
if not developer:
|
||||
print("Error: Developer not initialized")
|
||||
print(f"Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <your-name>")
|
||||
return 1
|
||||
|
||||
# Resolve spec base path (monorepo: spec/<package>, single-repo: spec)
|
||||
package = resolve_package(repo_root=repo_root)
|
||||
spec_base = get_spec_base(package, repo_root)
|
||||
|
||||
tasks_dir = get_tasks_dir(repo_root)
|
||||
task_dir = tasks_dir / TASK_NAME
|
||||
relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{TASK_NAME}"
|
||||
|
||||
# Check if already exists
|
||||
if task_dir.exists():
|
||||
print(f"Bootstrap task already exists: {relative_path}")
|
||||
return 0
|
||||
|
||||
# Create task directory
|
||||
task_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write files
|
||||
write_task_json(task_dir, developer, project_type, spec_base)
|
||||
write_prd(task_dir, project_type, spec_base)
|
||||
|
||||
# Set as current task
|
||||
set_current_task(relative_path, repo_root)
|
||||
|
||||
# Silent output - init command handles user-facing messages
|
||||
# Only output the task path for programmatic use
|
||||
print(relative_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
16
.trellis/scripts/get_context.py
Executable file
16
.trellis/scripts/get_context.py
Executable file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get Session Context for AI Agent.
|
||||
|
||||
Usage:
|
||||
python3 get_context.py Output context in text format
|
||||
python3 get_context.py --json Output context in JSON format
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from common.git_context import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
26
.trellis/scripts/get_developer.py
Executable file
26
.trellis/scripts/get_developer.py
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get current developer name.
|
||||
|
||||
This is a wrapper that uses common/paths.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from common.paths import get_developer
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI entry point."""
|
||||
developer = get_developer()
|
||||
if developer:
|
||||
print(developer)
|
||||
else:
|
||||
print("Developer not initialized", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
243
.trellis/scripts/hooks/linear_sync.py
Executable file
243
.trellis/scripts/hooks/linear_sync.py
Executable file
@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Linear sync hook for Trellis task lifecycle.
|
||||
|
||||
Syncs task events to Linear via the `linearis` CLI.
|
||||
|
||||
Usage (called automatically by task.py hooks):
|
||||
python3 .trellis/scripts/hooks/linear_sync.py create
|
||||
python3 .trellis/scripts/hooks/linear_sync.py start
|
||||
python3 .trellis/scripts/hooks/linear_sync.py archive
|
||||
|
||||
Manual usage:
|
||||
TASK_JSON_PATH=.trellis/tasks/<name>/task.json python3 .trellis/scripts/hooks/linear_sync.py sync
|
||||
|
||||
Environment:
|
||||
TASK_JSON_PATH - Absolute path to task.json (set by task.py)
|
||||
|
||||
Configuration:
|
||||
.trellis/hooks.local.json - Local config (gitignored), example:
|
||||
{
|
||||
"linear": {
|
||||
"team": "TEAM_KEY",
|
||||
"project": "Project Name",
|
||||
"assignees": {
|
||||
"dev-name": "linear-user-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# ─── Configuration ────────────────────────────────────────────────────────────
|
||||
|
||||
# Trellis priority → Linear priority (1=Urgent, 2=High, 3=Medium, 4=Low)
|
||||
PRIORITY_MAP = {"P0": 1, "P1": 2, "P2": 3, "P3": 4}
|
||||
|
||||
# Linear status names (must match your team's workflow)
|
||||
STATUS_IN_PROGRESS = "In Progress"
|
||||
STATUS_DONE = "Done"
|
||||
|
||||
|
||||
def _load_config() -> dict:
|
||||
"""Load local hook config from .trellis/hooks.local.json."""
|
||||
task_json_path = os.environ.get("TASK_JSON_PATH", "")
|
||||
if task_json_path:
|
||||
# Walk up from task.json to find .trellis/
|
||||
trellis_dir = Path(task_json_path).parent.parent.parent
|
||||
else:
|
||||
trellis_dir = Path(".trellis")
|
||||
|
||||
config_path = trellis_dir / "hooks.local.json"
|
||||
try:
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
CONFIG = _load_config()
|
||||
LINEAR_CFG = CONFIG.get("linear", {})
|
||||
|
||||
TEAM = LINEAR_CFG.get("team", "")
|
||||
PROJECT = LINEAR_CFG.get("project", "")
|
||||
ASSIGNEE_MAP = LINEAR_CFG.get("assignees", {})
|
||||
|
||||
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _read_task() -> tuple[dict, str]:
|
||||
path = os.environ.get("TASK_JSON_PATH", "")
|
||||
if not path:
|
||||
print("TASK_JSON_PATH not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return json.load(f), path
|
||||
|
||||
|
||||
def _write_task(data: dict, path: str) -> None:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def _linearis(*args: str) -> dict | None:
|
||||
result = subprocess.run(
|
||||
["linearis", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"linearis error: {result.stderr.strip()}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
stdout = result.stdout.strip()
|
||||
if stdout:
|
||||
return json.loads(stdout)
|
||||
return None
|
||||
|
||||
|
||||
def _get_linear_issue(task: dict) -> str | None:
|
||||
meta = task.get("meta")
|
||||
if isinstance(meta, dict):
|
||||
return meta.get("linear_issue")
|
||||
return None
|
||||
|
||||
|
||||
# ─── Actions ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def cmd_create() -> None:
|
||||
if not TEAM:
|
||||
print("No linear.team configured in hooks.local.json", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
task, path = _read_task()
|
||||
|
||||
# Skip if already linked
|
||||
if _get_linear_issue(task):
|
||||
print(f"Already linked: {_get_linear_issue(task)}")
|
||||
return
|
||||
|
||||
title = task.get("title") or task.get("name") or "Untitled"
|
||||
args = ["issues", "create", title, "--team", TEAM]
|
||||
|
||||
# Map priority
|
||||
priority = PRIORITY_MAP.get(task.get("priority", ""), 0)
|
||||
if priority:
|
||||
args.extend(["-p", str(priority)])
|
||||
|
||||
# Set project
|
||||
if PROJECT:
|
||||
args.extend(["--project", PROJECT])
|
||||
|
||||
# Assign to Linear user
|
||||
assignee = task.get("assignee", "")
|
||||
linear_user_id = ASSIGNEE_MAP.get(assignee)
|
||||
if linear_user_id:
|
||||
args.extend(["--assignee", linear_user_id])
|
||||
|
||||
# Link to parent's Linear issue if available
|
||||
parent_issue = _resolve_parent_linear_issue(task)
|
||||
if parent_issue:
|
||||
args.extend(["--parent-ticket", parent_issue])
|
||||
|
||||
result = _linearis(*args)
|
||||
if result and "identifier" in result:
|
||||
if not isinstance(task.get("meta"), dict):
|
||||
task["meta"] = {}
|
||||
task["meta"]["linear_issue"] = result["identifier"]
|
||||
_write_task(task, path)
|
||||
print(f"Created Linear issue: {result['identifier']}")
|
||||
|
||||
|
||||
def cmd_start() -> None:
|
||||
task, _ = _read_task()
|
||||
issue = _get_linear_issue(task)
|
||||
if not issue:
|
||||
return
|
||||
_linearis("issues", "update", issue, "-s", STATUS_IN_PROGRESS)
|
||||
print(f"Updated {issue} -> {STATUS_IN_PROGRESS}")
|
||||
cmd_sync()
|
||||
|
||||
|
||||
def cmd_archive() -> None:
|
||||
task, _ = _read_task()
|
||||
issue = _get_linear_issue(task)
|
||||
if not issue:
|
||||
return
|
||||
_linearis("issues", "update", issue, "-s", STATUS_DONE)
|
||||
print(f"Updated {issue} -> {STATUS_DONE}")
|
||||
|
||||
|
||||
def cmd_sync() -> None:
|
||||
"""Sync prd.md content to Linear issue description."""
|
||||
task, _ = _read_task()
|
||||
issue = _get_linear_issue(task)
|
||||
if not issue:
|
||||
print("No linear_issue in meta, run create first", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Find prd.md next to task.json
|
||||
task_json_path = os.environ.get("TASK_JSON_PATH", "")
|
||||
prd_path = Path(task_json_path).parent / "prd.md"
|
||||
if not prd_path.is_file():
|
||||
print(f"No prd.md found at {prd_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
description = prd_path.read_text(encoding="utf-8").strip()
|
||||
_linearis("issues", "update", issue, "-d", description)
|
||||
print(f"Synced prd.md to {issue} description")
|
||||
|
||||
|
||||
# ─── Parent Issue Resolution ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _resolve_parent_linear_issue(task: dict) -> str | None:
|
||||
"""Find parent task's Linear issue identifier."""
|
||||
parent_name = task.get("parent")
|
||||
if not parent_name:
|
||||
return None
|
||||
|
||||
task_json_path = os.environ.get("TASK_JSON_PATH", "")
|
||||
if not task_json_path:
|
||||
return None
|
||||
|
||||
current_task_dir = Path(task_json_path).parent
|
||||
tasks_dir = current_task_dir.parent
|
||||
parent_json = tasks_dir / parent_name / "task.json"
|
||||
|
||||
if parent_json.exists():
|
||||
try:
|
||||
with open(parent_json, encoding="utf-8") as f:
|
||||
parent_task = json.load(f)
|
||||
return _get_linear_issue(parent_task)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
action = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
actions = {
|
||||
"create": cmd_create,
|
||||
"start": cmd_start,
|
||||
"archive": cmd_archive,
|
||||
"sync": cmd_sync,
|
||||
}
|
||||
fn = actions.get(action)
|
||||
if fn:
|
||||
fn()
|
||||
else:
|
||||
print(f"Unknown action: {action}", file=sys.stderr)
|
||||
print(f"Valid actions: {', '.join(actions)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
51
.trellis/scripts/init_developer.py
Executable file
51
.trellis/scripts/init_developer.py
Executable file
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Initialize developer for workflow.
|
||||
|
||||
Usage:
|
||||
python3 init_developer.py <developer-name>
|
||||
|
||||
This creates:
|
||||
- .trellis/.developer file with developer info
|
||||
- .trellis/workspace/<name>/ directory structure
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from common.paths import (
|
||||
DIR_WORKFLOW,
|
||||
FILE_DEVELOPER,
|
||||
get_developer,
|
||||
)
|
||||
from common.developer import init_developer
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <developer-name>")
|
||||
print()
|
||||
print("Example:")
|
||||
print(f" {sys.argv[0]} john")
|
||||
sys.exit(1)
|
||||
|
||||
name = sys.argv[1]
|
||||
|
||||
# Check if already initialized
|
||||
existing = get_developer()
|
||||
if existing:
|
||||
print(f"Developer already initialized: {existing}")
|
||||
print()
|
||||
print(f"To reinitialize, remove {DIR_WORKFLOW}/{FILE_DEVELOPER} first")
|
||||
sys.exit(0)
|
||||
|
||||
if init_developer(name):
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
5
.trellis/scripts/multi_agent/__init__.py
Executable file
5
.trellis/scripts/multi_agent/__init__.py
Executable file
@ -0,0 +1,5 @@
|
||||
"""
|
||||
Multi-Agent Pipeline Scripts.
|
||||
|
||||
This module provides orchestration for multi-agent workflows.
|
||||
"""
|
||||
17
.trellis/scripts/multi_agent/_bootstrap.py
Executable file
17
.trellis/scripts/multi_agent/_bootstrap.py
Executable file
@ -0,0 +1,17 @@
|
||||
"""Bootstrap path setup for multi_agent scripts.
|
||||
|
||||
Import this module before importing from common/:
|
||||
|
||||
import _bootstrap # noqa: F401
|
||||
|
||||
This adds the parent scripts/ directory to sys.path so that
|
||||
`from common.xxx import yyy` works when running scripts directly
|
||||
via `python3 .trellis/scripts/multi_agent/some_script.py`.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_scripts_dir = str(Path(__file__).resolve().parent.parent)
|
||||
if _scripts_dir not in sys.path:
|
||||
sys.path.insert(0, _scripts_dir)
|
||||
398
.trellis/scripts/multi_agent/cleanup.py
Executable file
398
.trellis/scripts/multi_agent/cleanup.py
Executable file
@ -0,0 +1,398 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Agent Pipeline: Cleanup Worktree.
|
||||
|
||||
Usage:
|
||||
python3 cleanup.py <branch-name> Remove specific worktree
|
||||
python3 cleanup.py --list List all worktrees
|
||||
python3 cleanup.py --merged Remove merged worktrees
|
||||
python3 cleanup.py --all Remove all worktrees (with confirmation)
|
||||
|
||||
Options:
|
||||
-y, --yes Skip confirmation prompts
|
||||
--keep-branch Don't delete the git branch
|
||||
|
||||
This script:
|
||||
1. Archives task directory to archive/{YYYY-MM}/
|
||||
2. Removes agent from registry
|
||||
3. Removes git worktree
|
||||
4. Optionally deletes git branch
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import _bootstrap # noqa: F401 — adds parent scripts/ dir to sys.path
|
||||
|
||||
from common.git import run_git
|
||||
from common.log import Colors, log_info, log_success, log_warn, log_error
|
||||
from common.paths import FILE_TASK_JSON, get_repo_root
|
||||
from common.registry import (
|
||||
registry_get_file,
|
||||
registry_get_task_dir,
|
||||
registry_remove_by_id,
|
||||
registry_remove_by_worktree,
|
||||
registry_search_agent,
|
||||
)
|
||||
from common.task_utils import (
|
||||
archive_task_complete,
|
||||
is_safe_task_path,
|
||||
)
|
||||
|
||||
# Colors, log_info, log_success, log_warn, log_error
|
||||
# are now imported from common.log above.
|
||||
|
||||
|
||||
def confirm(prompt: str, skip_confirm: bool) -> bool:
|
||||
"""Ask for confirmation."""
|
||||
if skip_confirm:
|
||||
return True
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
log_error("Non-interactive mode detected. Use -y to skip confirmation.")
|
||||
return False
|
||||
|
||||
response = input(f"{prompt} [y/N] ")
|
||||
return response.lower() in ("y", "yes")
|
||||
|
||||
|
||||
def _warn_submodule_prs(task_dir_abs: Path) -> None:
|
||||
"""Print reminders for any open submodule PRs found in task.json."""
|
||||
task_json = task_dir_abs / FILE_TASK_JSON
|
||||
if not task_json.is_file():
|
||||
return
|
||||
|
||||
try:
|
||||
task_data = json.loads(task_json.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return
|
||||
|
||||
submodule_prs = task_data.get("submodule_prs")
|
||||
if not isinstance(submodule_prs, dict) or not submodule_prs:
|
||||
return
|
||||
|
||||
for name, url in submodule_prs.items():
|
||||
log_warn(f"Submodule PR still open: {name} -> {url}")
|
||||
log_info("Remember to close/merge submodule PRs before cleanup")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Commands
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def cmd_list(repo_root: Path) -> int:
|
||||
"""List worktrees."""
|
||||
print(f"{Colors.BLUE}=== Git Worktrees ==={Colors.NC}")
|
||||
print()
|
||||
|
||||
subprocess.run(["git", "worktree", "list"], cwd=repo_root)
|
||||
print()
|
||||
|
||||
# Show registry info
|
||||
registry_file = registry_get_file(repo_root)
|
||||
if registry_file and registry_file.is_file():
|
||||
print(f"{Colors.BLUE}=== Registered Agents ==={Colors.NC}")
|
||||
print()
|
||||
|
||||
data = json.loads(registry_file.read_text(encoding="utf-8"))
|
||||
agents = data.get("agents", [])
|
||||
|
||||
if agents:
|
||||
for agent in agents:
|
||||
print(
|
||||
f" {agent.get('id', '?')}: PID={agent.get('pid', '?')} [{agent.get('worktree_path', '?')}]"
|
||||
)
|
||||
else:
|
||||
print(" (none)")
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def archive_task(worktree_path: str, repo_root: Path) -> None:
|
||||
"""Archive task directory."""
|
||||
task_dir = registry_get_task_dir(worktree_path, repo_root)
|
||||
|
||||
if not task_dir or not is_safe_task_path(task_dir, repo_root):
|
||||
return
|
||||
|
||||
task_dir_abs = repo_root / task_dir
|
||||
if not task_dir_abs.is_dir():
|
||||
return
|
||||
|
||||
result = archive_task_complete(task_dir_abs, repo_root)
|
||||
if "archived_to" in result:
|
||||
dest = Path(result["archived_to"])
|
||||
log_success(f"Archived task: {dest.name} -> archive/{dest.parent.name}/")
|
||||
|
||||
|
||||
def cleanup_registry_only(search: str, repo_root: Path, skip_confirm: bool) -> int:
|
||||
"""Cleanup from registry only (no worktree)."""
|
||||
agent_info = registry_search_agent(search, repo_root)
|
||||
|
||||
if not agent_info:
|
||||
log_error(f"No agent found in registry matching: {search}")
|
||||
return 1
|
||||
|
||||
agent_id = agent_info.get("id", "?")
|
||||
task_dir = agent_info.get("task_dir", "?")
|
||||
|
||||
print()
|
||||
print(f"{Colors.BLUE}=== Cleanup Agent (no worktree) ==={Colors.NC}")
|
||||
print(f" Agent ID: {agent_id}")
|
||||
print(f" Task Dir: {task_dir}")
|
||||
print()
|
||||
|
||||
if not confirm("Archive task and remove from registry?", skip_confirm):
|
||||
log_info("Aborted")
|
||||
return 0
|
||||
|
||||
# Check for submodule PRs and archive task directory
|
||||
if task_dir and is_safe_task_path(task_dir, repo_root):
|
||||
task_dir_abs = repo_root / task_dir
|
||||
if task_dir_abs.is_dir():
|
||||
_warn_submodule_prs(task_dir_abs)
|
||||
result = archive_task_complete(task_dir_abs, repo_root)
|
||||
if "archived_to" in result:
|
||||
dest = Path(result["archived_to"])
|
||||
log_success(
|
||||
f"Archived task: {dest.name} -> archive/{dest.parent.name}/"
|
||||
)
|
||||
else:
|
||||
log_warn("Invalid task_dir in registry, skipping archive")
|
||||
|
||||
# Remove from registry
|
||||
registry_remove_by_id(agent_id, repo_root)
|
||||
log_success(f"Removed from registry: {agent_id}")
|
||||
|
||||
log_success("Cleanup complete")
|
||||
return 0
|
||||
|
||||
|
||||
def cleanup_worktree(
|
||||
branch: str, repo_root: Path, skip_confirm: bool, keep_branch: bool
|
||||
) -> int:
|
||||
"""Cleanup single worktree."""
|
||||
# Find worktree path for branch
|
||||
_, worktree_list, _ = run_git(
|
||||
["worktree", "list", "--porcelain"], cwd=repo_root
|
||||
)
|
||||
|
||||
worktree_path = None
|
||||
current_worktree = None
|
||||
|
||||
for line in worktree_list.splitlines():
|
||||
if line.startswith("worktree "):
|
||||
current_worktree = line[9:] # Remove "worktree " prefix
|
||||
elif line.startswith("branch refs/heads/"):
|
||||
current_branch = line[18:] # Remove "branch refs/heads/" prefix
|
||||
if current_branch == branch:
|
||||
worktree_path = current_worktree
|
||||
break
|
||||
|
||||
if not worktree_path:
|
||||
# No worktree found, try to cleanup from registry only
|
||||
log_warn(f"No worktree found for: {branch}")
|
||||
log_info("Trying to cleanup from registry...")
|
||||
return cleanup_registry_only(branch, repo_root, skip_confirm)
|
||||
|
||||
print()
|
||||
print(f"{Colors.BLUE}=== Cleanup Worktree ==={Colors.NC}")
|
||||
print(f" Branch: {branch}")
|
||||
print(f" Worktree: {worktree_path}")
|
||||
print()
|
||||
|
||||
if not confirm("Remove this worktree?", skip_confirm):
|
||||
log_info("Aborted")
|
||||
return 0
|
||||
|
||||
# 1. Archive task (and check for submodule PRs)
|
||||
task_dir = registry_get_task_dir(worktree_path, repo_root)
|
||||
if task_dir and is_safe_task_path(task_dir, repo_root):
|
||||
task_dir_abs_for_warn = repo_root / task_dir
|
||||
if task_dir_abs_for_warn.is_dir():
|
||||
_warn_submodule_prs(task_dir_abs_for_warn)
|
||||
archive_task(worktree_path, repo_root)
|
||||
|
||||
# 2. Remove from registry
|
||||
registry_remove_by_worktree(worktree_path, repo_root)
|
||||
log_info("Removed from registry")
|
||||
|
||||
# 3. Remove worktree
|
||||
log_info("Removing worktree...")
|
||||
ret, _, _ = run_git(
|
||||
["worktree", "remove", worktree_path, "--force"], cwd=repo_root
|
||||
)
|
||||
if ret != 0:
|
||||
# Try removing directory manually
|
||||
try:
|
||||
shutil.rmtree(worktree_path)
|
||||
except Exception as e:
|
||||
log_error(f"Failed to remove worktree: {e}")
|
||||
|
||||
log_success("Worktree removed")
|
||||
|
||||
# 4. Delete branch (optional)
|
||||
if not keep_branch:
|
||||
log_info("Deleting branch...")
|
||||
ret, _, _ = run_git(["branch", "-D", branch], cwd=repo_root)
|
||||
if ret != 0:
|
||||
log_warn("Could not delete branch (may be checked out elsewhere)")
|
||||
|
||||
log_success(f"Cleanup complete for: {branch}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_merged(repo_root: Path, skip_confirm: bool, keep_branch: bool) -> int:
|
||||
"""Cleanup merged worktrees."""
|
||||
# Get main branch
|
||||
_, head_out, _ = run_git(
|
||||
["symbolic-ref", "refs/remotes/origin/HEAD"], cwd=repo_root
|
||||
)
|
||||
main_branch = head_out.strip().replace("refs/remotes/origin/", "") or "main"
|
||||
|
||||
print(f"{Colors.BLUE}=== Finding Merged Worktrees ==={Colors.NC}")
|
||||
print()
|
||||
|
||||
# Get merged branches
|
||||
_, merged_out, _ = run_git(
|
||||
["branch", "--merged", main_branch], cwd=repo_root
|
||||
)
|
||||
merged_branches = []
|
||||
for line in merged_out.splitlines():
|
||||
branch = line.strip().lstrip("* ")
|
||||
if branch and branch != main_branch:
|
||||
merged_branches.append(branch)
|
||||
|
||||
if not merged_branches:
|
||||
log_info("No merged branches found")
|
||||
return 0
|
||||
|
||||
# Get worktree list
|
||||
_, worktree_list, _ = run_git(["worktree", "list"], cwd=repo_root)
|
||||
|
||||
worktree_branches = []
|
||||
for branch in merged_branches:
|
||||
if f"[{branch}]" in worktree_list:
|
||||
worktree_branches.append(branch)
|
||||
print(f" - {branch}")
|
||||
|
||||
if not worktree_branches:
|
||||
log_info("No merged worktrees found")
|
||||
return 0
|
||||
|
||||
print()
|
||||
if not confirm("Remove these merged worktrees?", skip_confirm):
|
||||
log_info("Aborted")
|
||||
return 0
|
||||
|
||||
for branch in worktree_branches:
|
||||
cleanup_worktree(branch, repo_root, True, keep_branch)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_all(repo_root: Path, skip_confirm: bool, keep_branch: bool) -> int:
|
||||
"""Cleanup all worktrees."""
|
||||
print(f"{Colors.BLUE}=== All Worktrees ==={Colors.NC}")
|
||||
print()
|
||||
|
||||
# Get worktree list
|
||||
_, worktree_list, _ = run_git(
|
||||
["worktree", "list", "--porcelain"], cwd=repo_root
|
||||
)
|
||||
|
||||
worktrees = []
|
||||
main_worktree = str(repo_root.resolve())
|
||||
|
||||
for line in worktree_list.splitlines():
|
||||
if line.startswith("worktree "):
|
||||
wt = line[9:]
|
||||
if wt != main_worktree:
|
||||
worktrees.append(wt)
|
||||
|
||||
if not worktrees:
|
||||
log_info("No worktrees to remove")
|
||||
return 0
|
||||
|
||||
for wt in worktrees:
|
||||
print(f" - {wt}")
|
||||
|
||||
print()
|
||||
print(f"{Colors.RED}WARNING: This will remove ALL worktrees!{Colors.NC}")
|
||||
|
||||
if not confirm("Are you sure?", skip_confirm):
|
||||
log_info("Aborted")
|
||||
return 0
|
||||
|
||||
# Get branch for each worktree
|
||||
for wt in worktrees:
|
||||
# Find branch name from worktree list
|
||||
_, wt_list, _ = run_git(["worktree", "list"], cwd=repo_root)
|
||||
for line in wt_list.splitlines():
|
||||
if wt in line:
|
||||
# Extract branch from [branch] format
|
||||
import re
|
||||
|
||||
match = re.search(r"\[([^\]]+)\]", line)
|
||||
if match:
|
||||
branch = match.group(1)
|
||||
cleanup_worktree(branch, repo_root, True, keep_branch)
|
||||
break
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Multi-Agent Pipeline: Cleanup Worktree"
|
||||
)
|
||||
parser.add_argument("branch", nargs="?", help="Branch name to cleanup")
|
||||
parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
|
||||
parser.add_argument(
|
||||
"--keep-branch", action="store_true", help="Don't delete git branch"
|
||||
)
|
||||
parser.add_argument("--list", action="store_true", help="List all worktrees")
|
||||
parser.add_argument("--merged", action="store_true", help="Remove merged worktrees")
|
||||
parser.add_argument("--all", action="store_true", help="Remove all worktrees")
|
||||
|
||||
args = parser.parse_args()
|
||||
repo_root = get_repo_root()
|
||||
|
||||
if args.list:
|
||||
return cmd_list(repo_root)
|
||||
elif args.merged:
|
||||
return cmd_merged(repo_root, args.yes, args.keep_branch)
|
||||
elif args.all:
|
||||
return cmd_all(repo_root, args.yes, args.keep_branch)
|
||||
elif args.branch:
|
||||
return cleanup_worktree(args.branch, repo_root, args.yes, args.keep_branch)
|
||||
else:
|
||||
print("""Usage:
|
||||
python3 cleanup.py <branch-name> Remove specific worktree
|
||||
python3 cleanup.py --list List all worktrees
|
||||
python3 cleanup.py --merged Remove merged worktrees
|
||||
python3 cleanup.py --all Remove all worktrees
|
||||
|
||||
Options:
|
||||
-y, --yes Skip confirmation
|
||||
--keep-branch Don't delete git branch
|
||||
""")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
620
.trellis/scripts/multi_agent/create_pr.py
Executable file
620
.trellis/scripts/multi_agent/create_pr.py
Executable file
@ -0,0 +1,620 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Agent Pipeline: Create PR.
|
||||
|
||||
Usage:
|
||||
python3 create_pr.py [task-dir] [--dry-run]
|
||||
|
||||
This script:
|
||||
1. Handles submodule changes (commit, push, PR) if any submodules are configured
|
||||
2. Stages and commits all main-repo changes (excluding workspace/)
|
||||
3. Pushes to origin
|
||||
4. Creates a Draft PR using `gh pr create`
|
||||
5. Updates task.json with status="completed", pr_url, submodule_prs, and current_phase
|
||||
|
||||
Note: This is the only action that performs git commit, as it's the final
|
||||
step after all implementation and checks are complete.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import _bootstrap # noqa: F401 — adds parent scripts/ dir to sys.path
|
||||
|
||||
from common.config import get_submodule_packages
|
||||
from common.git import run_git
|
||||
from common.io import read_json, write_json
|
||||
from common.log import Colors
|
||||
from common.paths import (
|
||||
DIR_WORKFLOW,
|
||||
FILE_TASK_JSON,
|
||||
get_current_task,
|
||||
get_repo_root,
|
||||
)
|
||||
from common.phase import get_phase_for_action
|
||||
|
||||
# Colors, read_json, write_json
|
||||
# are now imported from common.log and common.io above.
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Submodule PR Helpers
|
||||
# =============================================================================
|
||||
|
||||
# Warning message prepended to main PR body when submodule PRs exist
|
||||
_SUBMODULE_SQUASH_WARNING_MARKER = (
|
||||
"Merge submodule PR(s) first. If squash-merged, update submodule ref after merge."
|
||||
)
|
||||
|
||||
|
||||
def _get_submodule_default_branch(submodule_abs: Path) -> str:
|
||||
"""Get the default branch of a submodule repository.
|
||||
|
||||
Uses `git symbolic-ref refs/remotes/origin/HEAD` for portability
|
||||
(no grep, no English-dependent output).
|
||||
|
||||
Returns:
|
||||
Default branch name (e.g. "main"), falls back to "main" on failure.
|
||||
"""
|
||||
ret, out, _ = run_git(
|
||||
["symbolic-ref", "refs/remotes/origin/HEAD"], cwd=submodule_abs
|
||||
)
|
||||
if ret == 0 and out.strip():
|
||||
# Output: "refs/remotes/origin/main" -> "main"
|
||||
ref = out.strip()
|
||||
prefix = "refs/remotes/origin/"
|
||||
if ref.startswith(prefix):
|
||||
return ref[len(prefix):]
|
||||
return "main"
|
||||
|
||||
|
||||
def _process_submodule_changes(
|
||||
repo_root: Path,
|
||||
current_branch: str,
|
||||
commit_prefix: str,
|
||||
scope: str,
|
||||
task_name: str,
|
||||
task_data: dict,
|
||||
task_json: Path,
|
||||
dry_run: bool,
|
||||
) -> tuple[dict[str, str], list[str], bool]:
|
||||
"""Process submodule changes: commit, push, create PRs.
|
||||
|
||||
Returns:
|
||||
Tuple of (submodule_prs dict, changed_submodule_paths list, success bool).
|
||||
On failure, submodule_prs contains URLs persisted so far.
|
||||
"""
|
||||
submodule_packages = get_submodule_packages(repo_root)
|
||||
if not submodule_packages:
|
||||
return {}, [], True
|
||||
|
||||
# Load existing submodule_prs for incremental merge
|
||||
raw_prs = task_data.get("submodule_prs")
|
||||
submodule_prs: dict[str, str] = dict(raw_prs) if isinstance(raw_prs, dict) else {}
|
||||
|
||||
# Detect which submodules have changes
|
||||
changed: list[tuple[str, str]] = [] # (name, path)
|
||||
for pkg_name, pkg_path in submodule_packages.items():
|
||||
sub_abs = repo_root / pkg_path
|
||||
if not sub_abs.is_dir():
|
||||
continue
|
||||
|
||||
ret, status_out, _ = run_git(
|
||||
["status", "--porcelain"], cwd=sub_abs
|
||||
)
|
||||
if ret != 0:
|
||||
continue
|
||||
if status_out.strip():
|
||||
changed.append((pkg_name, pkg_path))
|
||||
|
||||
if not changed:
|
||||
return submodule_prs, [], True
|
||||
|
||||
# Determine submodule branch name: <repo-dir-name>/<main-branch>
|
||||
repo_dir_name = repo_root.name
|
||||
sub_branch = f"{repo_dir_name}/{current_branch}"
|
||||
|
||||
print(f"\n{Colors.BLUE}=== Submodule Changes Detected ==={Colors.NC}")
|
||||
for pkg_name, pkg_path in changed:
|
||||
print(f" - {pkg_name} ({pkg_path})")
|
||||
print()
|
||||
|
||||
changed_paths: list[str] = []
|
||||
|
||||
for pkg_name, pkg_path in changed:
|
||||
sub_abs = repo_root / pkg_path
|
||||
sub_base = _get_submodule_default_branch(sub_abs)
|
||||
sub_commit_msg = f"{commit_prefix}({scope}): {task_name}"
|
||||
|
||||
print(f"{Colors.YELLOW}Processing submodule: {pkg_name} ({pkg_path}){Colors.NC}")
|
||||
print(f" Submodule base branch: {sub_base}")
|
||||
print(f" Submodule branch: {sub_branch}")
|
||||
|
||||
if dry_run:
|
||||
print(f" [DRY-RUN] Would checkout branch: {sub_branch}")
|
||||
print(f" [DRY-RUN] Would commit: {sub_commit_msg}")
|
||||
print(f" [DRY-RUN] Would push to: origin/{sub_branch}")
|
||||
print(f" [DRY-RUN] Would create PR: {sub_branch} -> {sub_base}")
|
||||
submodule_prs[pkg_name] = "https://github.com/example/repo/pull/DRY-RUN"
|
||||
changed_paths.append(pkg_path)
|
||||
continue
|
||||
|
||||
# --- Checkout or create branch in submodule ---
|
||||
ret, _, _ = run_git(
|
||||
["show-ref", "--verify", "--quiet", f"refs/heads/{sub_branch}"],
|
||||
cwd=sub_abs,
|
||||
)
|
||||
if ret == 0:
|
||||
# Branch exists, checkout
|
||||
ret, _, err = run_git(
|
||||
["checkout", sub_branch], cwd=sub_abs
|
||||
)
|
||||
if ret != 0:
|
||||
print(f"{Colors.RED}Failed to checkout branch in {pkg_name}: {err}{Colors.NC}")
|
||||
return submodule_prs, changed_paths, False
|
||||
|
||||
# Check for divergence (reuse risk)
|
||||
ret_anc, _, _ = run_git(
|
||||
["merge-base", "--is-ancestor", sub_base, sub_branch],
|
||||
cwd=sub_abs,
|
||||
)
|
||||
if ret_anc != 0:
|
||||
print(
|
||||
f" {Colors.YELLOW}[WARN] submodule branch has diverged history, "
|
||||
f"consider recreating{Colors.NC}"
|
||||
)
|
||||
else:
|
||||
# Create new branch
|
||||
ret, _, err = run_git(
|
||||
["checkout", "-b", sub_branch], cwd=sub_abs
|
||||
)
|
||||
if ret != 0:
|
||||
print(f"{Colors.RED}Failed to create branch in {pkg_name}: {err}{Colors.NC}")
|
||||
return submodule_prs, changed_paths, False
|
||||
|
||||
# --- Stage and commit ---
|
||||
run_git(["add", "-A"], cwd=sub_abs)
|
||||
|
||||
ret, _, _ = run_git(["diff", "--cached", "--quiet"], cwd=sub_abs)
|
||||
if ret != 0:
|
||||
# Has staged changes
|
||||
ret, _, err = run_git(
|
||||
["commit", "-m", sub_commit_msg], cwd=sub_abs
|
||||
)
|
||||
if ret != 0:
|
||||
print(f"{Colors.RED}Failed to commit in {pkg_name}: {err}{Colors.NC}")
|
||||
return submodule_prs, changed_paths, False
|
||||
print(f" {Colors.GREEN}Committed in {pkg_name}{Colors.NC}")
|
||||
else:
|
||||
print(f" No staged changes in {pkg_name}, skipping commit")
|
||||
|
||||
# --- Push ---
|
||||
ret, _, err = run_git(
|
||||
["push", "-u", "origin", sub_branch], cwd=sub_abs
|
||||
)
|
||||
if ret != 0:
|
||||
print(f"{Colors.RED}Failed to push {pkg_name}: {err}{Colors.NC}")
|
||||
return submodule_prs, changed_paths, False
|
||||
print(f" {Colors.GREEN}Pushed {pkg_name} to origin/{sub_branch}{Colors.NC}")
|
||||
|
||||
# --- Create or reuse PR ---
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh", "pr", "list",
|
||||
"--head", sub_branch,
|
||||
"--base", sub_base,
|
||||
"--json", "url",
|
||||
"--jq", ".[0].url",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
cwd=str(sub_abs),
|
||||
)
|
||||
existing_sub_pr = result.stdout.strip()
|
||||
|
||||
if existing_sub_pr:
|
||||
print(f" {Colors.YELLOW}PR already exists: {existing_sub_pr}{Colors.NC}")
|
||||
sub_pr_url = existing_sub_pr
|
||||
else:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh", "pr", "create",
|
||||
"--draft",
|
||||
"--base", sub_base,
|
||||
"--title", f"{commit_prefix}({scope}): {task_name} [{pkg_name}]",
|
||||
"--body", f"Submodule changes for {task_name}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
cwd=str(sub_abs),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(
|
||||
f"{Colors.RED}Failed to create PR for {pkg_name}: "
|
||||
f"{result.stderr}{Colors.NC}"
|
||||
)
|
||||
return submodule_prs, changed_paths, False
|
||||
|
||||
sub_pr_url = result.stdout.strip()
|
||||
print(f" {Colors.GREEN}PR created for {pkg_name}: {sub_pr_url}{Colors.NC}")
|
||||
|
||||
# Persist immediately (incremental, supports re-entry)
|
||||
submodule_prs[pkg_name] = sub_pr_url
|
||||
task_data["submodule_prs"] = submodule_prs
|
||||
write_json(task_json, task_data)
|
||||
|
||||
changed_paths.append(pkg_path)
|
||||
|
||||
return submodule_prs, changed_paths, True
|
||||
|
||||
|
||||
def _build_submodule_warning(submodule_prs: dict[str, str]) -> str:
|
||||
"""Build the squash-merge warning block for the main PR body."""
|
||||
pr_lines = "\n".join(f"> - {name}: {url}" for name, url in submodule_prs.items())
|
||||
return (
|
||||
f"> {_SUBMODULE_SQUASH_WARNING_MARKER}\n"
|
||||
f">\n"
|
||||
f"> Submodule PRs:\n"
|
||||
f"{pr_lines}\n"
|
||||
f"\n---\n\n"
|
||||
)
|
||||
|
||||
|
||||
def _ensure_submodule_warning_on_existing_pr(
|
||||
submodule_prs: dict[str, str],
|
||||
dry_run: bool,
|
||||
) -> None:
|
||||
"""Read-modify-write: add squash warning to existing PR if missing."""
|
||||
if dry_run:
|
||||
print("[DRY-RUN] Would check/add submodule warning to existing PR")
|
||||
return
|
||||
|
||||
# Read current PR body
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh", "pr", "view",
|
||||
"--json", "body",
|
||||
"--jq", ".body",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return
|
||||
|
||||
current_body = result.stdout.strip()
|
||||
if _SUBMODULE_SQUASH_WARNING_MARKER in current_body:
|
||||
return # Warning already present
|
||||
|
||||
# Prepend warning to existing body
|
||||
warning = _build_submodule_warning(submodule_prs)
|
||||
new_body = warning + current_body
|
||||
|
||||
subprocess.run(
|
||||
["gh", "pr", "edit", "--body", new_body],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
print(f" {Colors.GREEN}Added submodule merge warning to existing PR{Colors.NC}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description="Multi-Agent Pipeline: Create PR")
|
||||
parser.add_argument("dir", nargs="?", help="Task directory")
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true", help="Show what would be done"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
repo_root = get_repo_root()
|
||||
|
||||
# =============================================================================
|
||||
# Get Task Directory
|
||||
# =============================================================================
|
||||
target_dir = args.dir
|
||||
if not target_dir:
|
||||
# Try to get from .current-task
|
||||
current_task = get_current_task(repo_root)
|
||||
if current_task:
|
||||
target_dir = current_task
|
||||
|
||||
if not target_dir:
|
||||
print(
|
||||
f"{Colors.RED}Error: No task directory specified and no current task set{Colors.NC}"
|
||||
)
|
||||
print("Usage: python3 create_pr.py [task-dir] [--dry-run]")
|
||||
return 1
|
||||
|
||||
# Support relative paths
|
||||
if not target_dir.startswith("/"):
|
||||
target_dir_path = repo_root / target_dir
|
||||
else:
|
||||
target_dir_path = Path(target_dir)
|
||||
|
||||
task_json = target_dir_path / FILE_TASK_JSON
|
||||
if not task_json.is_file():
|
||||
print(f"{Colors.RED}Error: task.json not found at {target_dir_path}{Colors.NC}")
|
||||
return 1
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
print(f"{Colors.BLUE}=== Create PR ==={Colors.NC}")
|
||||
if args.dry_run:
|
||||
print(
|
||||
f"{Colors.YELLOW}[DRY-RUN MODE] No actual changes will be made{Colors.NC}"
|
||||
)
|
||||
print()
|
||||
|
||||
# Read task config
|
||||
task_data = read_json(task_json)
|
||||
if not task_data:
|
||||
print(f"{Colors.RED}Error: Failed to read task.json{Colors.NC}")
|
||||
return 1
|
||||
|
||||
task_name = task_data.get("name", "")
|
||||
base_branch = task_data.get("base_branch", "main")
|
||||
scope = task_data.get("scope", "core")
|
||||
dev_type = task_data.get("dev_type", "feature")
|
||||
|
||||
# Map dev_type to commit prefix
|
||||
prefix_map = {
|
||||
"feature": "feat",
|
||||
"frontend": "feat",
|
||||
"backend": "feat",
|
||||
"fullstack": "feat",
|
||||
"bugfix": "fix",
|
||||
"fix": "fix",
|
||||
"refactor": "refactor",
|
||||
"docs": "docs",
|
||||
"test": "test",
|
||||
}
|
||||
commit_prefix = prefix_map.get(dev_type, "feat")
|
||||
|
||||
print(f"Task: {task_name}")
|
||||
print(f"Base branch: {base_branch}")
|
||||
print(f"Scope: {scope}")
|
||||
print(f"Commit prefix: {commit_prefix}")
|
||||
print()
|
||||
|
||||
# Get current branch
|
||||
_, branch_out, _ = run_git(["branch", "--show-current"])
|
||||
current_branch = branch_out.strip()
|
||||
print(f"Current branch: {current_branch}")
|
||||
|
||||
# =============================================================================
|
||||
# Submodule PR Flow (runs BEFORE main repo staging)
|
||||
# =============================================================================
|
||||
submodule_prs, changed_submodule_paths, sub_success = _process_submodule_changes(
|
||||
repo_root=repo_root,
|
||||
current_branch=current_branch,
|
||||
commit_prefix=commit_prefix,
|
||||
scope=scope,
|
||||
task_name=task_name,
|
||||
task_data=task_data,
|
||||
task_json=task_json,
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
|
||||
if not sub_success:
|
||||
print(
|
||||
f"\n{Colors.RED}Submodule PR flow failed. "
|
||||
f"Skipping main repo commit/PR.{Colors.NC}"
|
||||
)
|
||||
print("Already-created submodule PRs have been saved to task.json.")
|
||||
return 1
|
||||
|
||||
# =============================================================================
|
||||
# Main Repo: Stage, Commit, Push, PR
|
||||
# =============================================================================
|
||||
|
||||
# Check for changes
|
||||
print(f"{Colors.YELLOW}Checking for changes...{Colors.NC}")
|
||||
|
||||
# Stage changes
|
||||
run_git(["add", "-A"])
|
||||
|
||||
# Exclude workspace and temp files
|
||||
run_git(["reset", f"{DIR_WORKFLOW}/workspace/"])
|
||||
run_git(["reset", ".agent-log", ".session-id"])
|
||||
|
||||
# If submodules changed, ensure their ref updates are staged
|
||||
for sub_path in changed_submodule_paths:
|
||||
run_git(["add", sub_path])
|
||||
|
||||
# Check if there are staged changes
|
||||
ret, _, _ = run_git(["diff", "--cached", "--quiet"])
|
||||
has_staged_changes = ret != 0
|
||||
|
||||
if not has_staged_changes:
|
||||
print(f"{Colors.YELLOW}No staged changes to commit{Colors.NC}")
|
||||
|
||||
# Check for unpushed commits
|
||||
ret, log_out, _ = run_git(
|
||||
["log", f"origin/{current_branch}..HEAD", "--oneline"]
|
||||
)
|
||||
unpushed = len([line for line in log_out.splitlines() if line.strip()])
|
||||
|
||||
if unpushed == 0:
|
||||
if args.dry_run:
|
||||
run_git(["reset", "HEAD"])
|
||||
print(f"{Colors.RED}No changes to create PR{Colors.NC}")
|
||||
return 1
|
||||
|
||||
print(f"Found {unpushed} unpushed commit(s)")
|
||||
else:
|
||||
# Commit changes
|
||||
print(f"{Colors.YELLOW}Committing changes...{Colors.NC}")
|
||||
commit_msg = f"{commit_prefix}({scope}): {task_name}"
|
||||
|
||||
if args.dry_run:
|
||||
print(f"[DRY-RUN] Would commit with message: {commit_msg}")
|
||||
print("[DRY-RUN] Staged files:")
|
||||
_, staged_out, _ = run_git(["diff", "--cached", "--name-only"])
|
||||
for line in staged_out.splitlines():
|
||||
print(f" - {line}")
|
||||
else:
|
||||
run_git(["commit", "-m", commit_msg])
|
||||
print(f"{Colors.GREEN}Committed: {commit_msg}{Colors.NC}")
|
||||
|
||||
# Push to remote
|
||||
print(f"{Colors.YELLOW}Pushing to remote...{Colors.NC}")
|
||||
if args.dry_run:
|
||||
print(f"[DRY-RUN] Would push to: origin/{current_branch}")
|
||||
else:
|
||||
ret, _, err = run_git(["push", "-u", "origin", current_branch])
|
||||
if ret != 0:
|
||||
print(f"{Colors.RED}Failed to push: {err}{Colors.NC}")
|
||||
return 1
|
||||
print(f"{Colors.GREEN}Pushed to origin/{current_branch}{Colors.NC}")
|
||||
|
||||
# Create PR
|
||||
print(f"{Colors.YELLOW}Creating PR...{Colors.NC}")
|
||||
pr_title = f"{commit_prefix}({scope}): {task_name}"
|
||||
pr_url = ""
|
||||
|
||||
# Build PR body with optional submodule warning
|
||||
has_submodule_prs = bool(submodule_prs)
|
||||
|
||||
if args.dry_run:
|
||||
print("[DRY-RUN] Would create PR:")
|
||||
print(f" Title: {pr_title}")
|
||||
print(f" Base: {base_branch}")
|
||||
print(f" Head: {current_branch}")
|
||||
prd_file = target_dir_path / "prd.md"
|
||||
if prd_file.is_file():
|
||||
print(" Body: (from prd.md)")
|
||||
if has_submodule_prs:
|
||||
print(" Body includes submodule merge warning")
|
||||
pr_url = "https://github.com/example/repo/pull/DRY-RUN"
|
||||
else:
|
||||
# Check if PR already exists
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"list",
|
||||
"--head",
|
||||
current_branch,
|
||||
"--base",
|
||||
base_branch,
|
||||
"--json",
|
||||
"url",
|
||||
"--jq",
|
||||
".[0].url",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
existing_pr = result.stdout.strip()
|
||||
|
||||
if existing_pr:
|
||||
print(f"{Colors.YELLOW}PR already exists: {existing_pr}{Colors.NC}")
|
||||
pr_url = existing_pr
|
||||
|
||||
# Read-modify-write: add submodule warning if missing
|
||||
if has_submodule_prs:
|
||||
_ensure_submodule_warning_on_existing_pr(
|
||||
submodule_prs, args.dry_run
|
||||
)
|
||||
else:
|
||||
# Read PRD as PR body
|
||||
pr_body = ""
|
||||
prd_file = target_dir_path / "prd.md"
|
||||
if prd_file.is_file():
|
||||
pr_body = prd_file.read_text(encoding="utf-8")
|
||||
|
||||
# Prepend submodule warning if applicable
|
||||
if has_submodule_prs:
|
||||
pr_body = _build_submodule_warning(submodule_prs) + pr_body
|
||||
|
||||
# Create PR
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"create",
|
||||
"--draft",
|
||||
"--base",
|
||||
base_branch,
|
||||
"--title",
|
||||
pr_title,
|
||||
"--body",
|
||||
pr_body,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"{Colors.RED}Failed to create PR: {result.stderr}{Colors.NC}")
|
||||
return 1
|
||||
|
||||
pr_url = result.stdout.strip()
|
||||
print(f"{Colors.GREEN}PR created: {pr_url}{Colors.NC}")
|
||||
|
||||
# Update task.json
|
||||
print(f"{Colors.YELLOW}Updating task status...{Colors.NC}")
|
||||
if args.dry_run:
|
||||
print("[DRY-RUN] Would update task.json:")
|
||||
print(" status: completed")
|
||||
print(f" pr_url: {pr_url}")
|
||||
if has_submodule_prs:
|
||||
print(f" submodule_prs: {submodule_prs}")
|
||||
print(" current_phase: (set to create-pr phase)")
|
||||
else:
|
||||
# Get the phase number for create-pr action
|
||||
create_pr_phase = get_phase_for_action(task_json, "create-pr")
|
||||
if not create_pr_phase:
|
||||
create_pr_phase = 4 # Default fallback
|
||||
|
||||
task_data["status"] = "completed"
|
||||
task_data["pr_url"] = pr_url
|
||||
task_data["current_phase"] = create_pr_phase
|
||||
if has_submodule_prs:
|
||||
task_data["submodule_prs"] = submodule_prs
|
||||
|
||||
write_json(task_json, task_data)
|
||||
print(
|
||||
f"{Colors.GREEN}Task status updated to 'completed', phase {create_pr_phase}{Colors.NC}"
|
||||
)
|
||||
|
||||
# In dry-run, reset the staging area
|
||||
if args.dry_run:
|
||||
run_git(["reset", "HEAD"])
|
||||
|
||||
print()
|
||||
print(f"{Colors.GREEN}=== PR Created Successfully ==={Colors.NC}")
|
||||
print(f"PR URL: {pr_url}")
|
||||
if has_submodule_prs:
|
||||
print("Submodule PRs:")
|
||||
for name, url in submodule_prs.items():
|
||||
print(f" - {name}: {url}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
213
.trellis/scripts/multi_agent/plan.py
Executable file
213
.trellis/scripts/multi_agent/plan.py
Executable file
@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Agent Pipeline: Plan Agent Launcher.
|
||||
|
||||
Usage: python3 plan.py --name <task-name> --type <dev-type> --requirement "<requirement>"
|
||||
|
||||
This script:
|
||||
1. Creates task directory
|
||||
2. Starts Plan Agent in background
|
||||
3. Plan Agent produces fully configured task directory
|
||||
|
||||
After completion, use start.py to launch the Dispatch Agent.
|
||||
|
||||
Prerequisites:
|
||||
- agents/plan.md must exist (in .claude/, .cursor/, .iflow/, or .opencode/)
|
||||
- Developer must be initialized
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import _bootstrap # noqa: F401 — adds parent scripts/ dir to sys.path
|
||||
|
||||
from common.cli_adapter import get_cli_adapter
|
||||
from common.log import Colors, log_info, log_success, log_error
|
||||
from common.paths import get_repo_root
|
||||
from common.developer import ensure_developer
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
DEFAULT_PLATFORM = "claude"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Multi-Agent Pipeline: Plan Agent Launcher"
|
||||
)
|
||||
parser.add_argument("--name", "-n", required=True, help="Task name (e.g., user-auth)")
|
||||
parser.add_argument("--type", "-t", required=True, help="Dev type: backend|frontend|fullstack")
|
||||
parser.add_argument("--requirement", "-r", required=True, help="Requirement description")
|
||||
parser.add_argument(
|
||||
"--platform", "-p",
|
||||
choices=["claude", "cursor", "iflow", "opencode", "codex", "qoder"],
|
||||
default=DEFAULT_PLATFORM,
|
||||
help="Platform to use (default: claude)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
task_name = args.name
|
||||
dev_type = args.type
|
||||
requirement = args.requirement
|
||||
platform = args.platform
|
||||
|
||||
# Initialize CLI adapter
|
||||
adapter = get_cli_adapter(platform)
|
||||
|
||||
# Validate dev type
|
||||
if dev_type not in ("backend", "frontend", "fullstack"):
|
||||
log_error(f"Invalid dev type: {dev_type} (must be: backend, frontend, fullstack)")
|
||||
return 1
|
||||
|
||||
project_root = get_repo_root()
|
||||
|
||||
# Check plan agent exists (path varies by platform)
|
||||
if adapter.requires_agent_definition_file:
|
||||
plan_md = adapter.get_agent_path("plan", project_root)
|
||||
if not plan_md.is_file():
|
||||
log_error(f"Agent definition not found at {plan_md}")
|
||||
log_info(f"Platform: {platform}")
|
||||
return 1
|
||||
|
||||
ensure_developer(project_root)
|
||||
|
||||
# =============================================================================
|
||||
# Step 1: Create Task Directory
|
||||
# =============================================================================
|
||||
print()
|
||||
print(f"{Colors.BLUE}=== Multi-Agent Pipeline: Plan ==={Colors.NC}")
|
||||
log_info(f"Task: {task_name}")
|
||||
log_info(f"Type: {dev_type}")
|
||||
log_info(f"Requirement: {requirement}")
|
||||
print()
|
||||
|
||||
log_info("Step 1: Creating task directory...")
|
||||
|
||||
# Import task module to create task
|
||||
from task import cmd_create
|
||||
import argparse as ap
|
||||
|
||||
# Create task using task.py's create command
|
||||
create_args = ap.Namespace(
|
||||
title=requirement,
|
||||
slug=task_name,
|
||||
assignee=None,
|
||||
priority="P2",
|
||||
description=""
|
||||
)
|
||||
|
||||
# Capture stdout to get task dir
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
stdout_capture = io.StringIO()
|
||||
with redirect_stdout(stdout_capture):
|
||||
ret = cmd_create(create_args)
|
||||
|
||||
if ret != 0:
|
||||
log_error("Failed to create task directory")
|
||||
return 1
|
||||
|
||||
task_dir = stdout_capture.getvalue().strip().split("\n")[-1]
|
||||
task_dir_abs = project_root / task_dir
|
||||
|
||||
log_success(f"Task directory: {task_dir}")
|
||||
|
||||
# =============================================================================
|
||||
# Step 2: Prepare and Start Plan Agent
|
||||
# =============================================================================
|
||||
log_info("Step 2: Starting Plan Agent in background...")
|
||||
|
||||
log_file = task_dir_abs / ".plan-log"
|
||||
log_file.touch()
|
||||
|
||||
# Get proxy environment variables
|
||||
https_proxy = os.environ.get("https_proxy", "")
|
||||
http_proxy = os.environ.get("http_proxy", "")
|
||||
all_proxy = os.environ.get("all_proxy", "")
|
||||
|
||||
# Start agent in background (cross-platform, no shell script needed)
|
||||
env = os.environ.copy()
|
||||
env["PLAN_TASK_NAME"] = task_name
|
||||
env["PLAN_DEV_TYPE"] = dev_type
|
||||
env["PLAN_TASK_DIR"] = task_dir
|
||||
env["PLAN_REQUIREMENT"] = requirement
|
||||
env["https_proxy"] = https_proxy
|
||||
env["http_proxy"] = http_proxy
|
||||
env["all_proxy"] = all_proxy
|
||||
|
||||
# Clear nested-session detection so the new CLI process can start
|
||||
env.pop("CLAUDECODE", None)
|
||||
|
||||
# Set non-interactive env var based on platform
|
||||
env.update(adapter.get_non_interactive_env())
|
||||
|
||||
# Build CLI command using adapter
|
||||
cli_cmd = adapter.build_run_command(
|
||||
agent="plan", # Will be mapped to "trellis-plan" for OpenCode
|
||||
prompt=f"Start planning for task: {task_name}",
|
||||
skip_permissions=True,
|
||||
verbose=True,
|
||||
json_output=True,
|
||||
)
|
||||
|
||||
with log_file.open("w") as log_f:
|
||||
# Use shell=False for cross-platform compatibility
|
||||
# creationflags for Windows, start_new_session for Unix
|
||||
popen_kwargs = {
|
||||
"stdout": log_f,
|
||||
"stderr": subprocess.STDOUT,
|
||||
"cwd": str(project_root),
|
||||
"env": env,
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
else:
|
||||
popen_kwargs["start_new_session"] = True
|
||||
|
||||
process = subprocess.Popen(cli_cmd, **popen_kwargs)
|
||||
agent_pid = process.pid
|
||||
|
||||
log_success(f"Plan Agent started (PID: {agent_pid})")
|
||||
|
||||
# =============================================================================
|
||||
# Summary
|
||||
# =============================================================================
|
||||
print()
|
||||
print(f"{Colors.GREEN}=== Plan Agent Running ==={Colors.NC}")
|
||||
print()
|
||||
print(f" Task: {task_name}")
|
||||
print(f" Type: {dev_type}")
|
||||
print(f" Dir: {task_dir}")
|
||||
print(f" Log: {log_file}")
|
||||
print(f" PID: {agent_pid}")
|
||||
print()
|
||||
print(f"{Colors.YELLOW}To monitor:{Colors.NC}")
|
||||
print(f" tail -f {log_file}")
|
||||
print()
|
||||
print(f"{Colors.YELLOW}To check status:{Colors.NC}")
|
||||
print(f" ps -p {agent_pid}")
|
||||
print(f" ls -la {task_dir}")
|
||||
print()
|
||||
print(f"{Colors.YELLOW}After completion, run:{Colors.NC}")
|
||||
print(f" python3 ./.trellis/scripts/multi_agent/start.py {task_dir}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
539
.trellis/scripts/multi_agent/start.py
Executable file
539
.trellis/scripts/multi_agent/start.py
Executable file
@ -0,0 +1,539 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Agent Pipeline: Start Worktree Agent.
|
||||
|
||||
Usage: python3 start.py <task-dir>
|
||||
Example: python3 start.py .trellis/tasks/01-21-my-task
|
||||
|
||||
This script:
|
||||
1. Creates worktree (if not exists) with dependency install
|
||||
2. Copies environment files (from worktree.yaml config)
|
||||
3. Sets .current-task in worktree
|
||||
4. Starts claude agent in background
|
||||
5. Registers agent to registry.json
|
||||
|
||||
Prerequisites:
|
||||
- task.json must exist with 'branch' field
|
||||
- agents/dispatch.md must exist (in .claude/, .cursor/, .iflow/, or .opencode/)
|
||||
|
||||
Configuration: .trellis/worktree.yaml
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import _bootstrap # noqa: F401 — adds parent scripts/ dir to sys.path
|
||||
|
||||
from common.cli_adapter import get_cli_adapter
|
||||
from common.git import run_git
|
||||
from common.io import read_json, write_json
|
||||
from common.log import Colors, log_info, log_success, log_warn, log_error
|
||||
from common.paths import (
|
||||
DIR_WORKFLOW,
|
||||
FILE_CURRENT_TASK,
|
||||
FILE_TASK_JSON,
|
||||
get_repo_root,
|
||||
)
|
||||
from common.registry import (
|
||||
registry_add_agent,
|
||||
registry_get_file,
|
||||
)
|
||||
from common.config import (
|
||||
get_default_package,
|
||||
get_packages,
|
||||
get_submodule_packages,
|
||||
validate_package,
|
||||
)
|
||||
from common.worktree import (
|
||||
get_worktree_base_dir,
|
||||
get_worktree_config,
|
||||
get_worktree_copy_files,
|
||||
get_worktree_post_create_hooks,
|
||||
)
|
||||
|
||||
# Colors, log_info, log_success, log_warn, log_error, read_json, write_json
|
||||
# are now imported from common.log and common.io above.
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
DEFAULT_PLATFORM = "claude"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Submodule Init
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _init_submodules_for_task(
|
||||
task_data: dict, worktree_path: str, project_root: Path
|
||||
) -> None:
|
||||
"""Initialize submodules in worktree based on task's target package.
|
||||
|
||||
Resolves the target package from task_data.package -> default_package -> None.
|
||||
Only initializes submodule-type packages. Idempotent: skips already-initialized
|
||||
submodules to avoid detaching HEAD on in-progress work.
|
||||
"""
|
||||
# Skip if not a monorepo (no packages configured)
|
||||
if get_packages(project_root) is None:
|
||||
return
|
||||
|
||||
# Resolve package: task.package -> default_package -> None
|
||||
task_package = task_data.get("package")
|
||||
package = None
|
||||
|
||||
if task_package and isinstance(task_package, str):
|
||||
if validate_package(task_package, project_root):
|
||||
package = task_package
|
||||
else:
|
||||
log_warn(
|
||||
f"package '{task_package}' not found in config.yaml, "
|
||||
"skipping submodule init"
|
||||
)
|
||||
return
|
||||
else:
|
||||
# Fallback to default_package
|
||||
default_pkg = get_default_package(project_root)
|
||||
if default_pkg:
|
||||
if validate_package(default_pkg, project_root):
|
||||
package = default_pkg
|
||||
else:
|
||||
log_warn(
|
||||
f"package '{default_pkg}' not found in config.yaml, "
|
||||
"skipping submodule init"
|
||||
)
|
||||
return
|
||||
|
||||
if not package:
|
||||
log_warn("no package specified, skipping submodule init")
|
||||
return
|
||||
|
||||
# Check if this package is a submodule
|
||||
submodule_packages = get_submodule_packages(project_root)
|
||||
if package not in submodule_packages:
|
||||
log_info(f"Package '{package}' is not a submodule, skipping submodule init")
|
||||
return
|
||||
|
||||
submodule_path = submodule_packages[package]
|
||||
log_info(f"Checking submodule status for '{package}' ({submodule_path})...")
|
||||
|
||||
# Run git submodule status in worktree directory
|
||||
ret, status_out, status_err = run_git(
|
||||
["submodule", "status", submodule_path], cwd=Path(worktree_path)
|
||||
)
|
||||
|
||||
if ret != 0:
|
||||
log_warn(
|
||||
f"git submodule status failed for '{submodule_path}': {status_err.strip()}, "
|
||||
"skipping submodule init"
|
||||
)
|
||||
return
|
||||
|
||||
# Parse the prefix character from submodule status output
|
||||
# Format: "<prefix><sha1> <path> (<describe>)"
|
||||
# Prefix: '-' (uninitialized), ' ' (normal), '+' (commit mismatch), 'U' (conflict)
|
||||
status_line = status_out.rstrip("\n\r")
|
||||
if not status_line:
|
||||
log_warn(f"Empty submodule status for '{submodule_path}', skipping")
|
||||
return
|
||||
|
||||
prefix = status_line[0]
|
||||
|
||||
if prefix == "-":
|
||||
# Uninitialized: run git submodule update --init
|
||||
log_info(f"Initializing submodule '{submodule_path}'...")
|
||||
ret, _, err = run_git(
|
||||
["submodule", "update", "--init", submodule_path],
|
||||
cwd=Path(worktree_path),
|
||||
)
|
||||
if ret != 0:
|
||||
log_warn(f"Failed to initialize submodule '{submodule_path}': {err.strip()}")
|
||||
else:
|
||||
log_success(f"Submodule '{submodule_path}' initialized")
|
||||
elif prefix == " ":
|
||||
log_info(f"Submodule '{submodule_path}' already initialized, skipping")
|
||||
elif prefix == "+":
|
||||
log_warn(
|
||||
f"submodule {submodule_path} has local changes, skipping update"
|
||||
)
|
||||
elif prefix == "U":
|
||||
log_warn(
|
||||
f"submodule {submodule_path} has conflicts, skipping"
|
||||
)
|
||||
else:
|
||||
log_warn(
|
||||
f"Unknown submodule status prefix '{prefix}' for '{submodule_path}', skipping"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Multi-Agent Pipeline: Start Worktree Agent")
|
||||
parser.add_argument("task_dir", help="Task directory path")
|
||||
parser.add_argument(
|
||||
"--platform", "-p",
|
||||
choices=["claude", "cursor", "iflow", "opencode", "codex", "qoder"],
|
||||
default=DEFAULT_PLATFORM,
|
||||
help="Platform to use (default: claude)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
task_dir_arg = args.task_dir
|
||||
platform = args.platform
|
||||
|
||||
# Initialize CLI adapter
|
||||
adapter = get_cli_adapter(platform)
|
||||
|
||||
project_root = get_repo_root()
|
||||
|
||||
# Normalize paths
|
||||
task_dir_path = Path(task_dir_arg)
|
||||
if task_dir_path.is_absolute():
|
||||
task_dir_abs = task_dir_path
|
||||
else:
|
||||
task_dir_abs = project_root / task_dir_path
|
||||
|
||||
try:
|
||||
task_dir_relative = task_dir_abs.relative_to(project_root).as_posix()
|
||||
except ValueError:
|
||||
task_dir_relative = str(task_dir_abs)
|
||||
|
||||
task_json_path = task_dir_abs / FILE_TASK_JSON
|
||||
|
||||
# =============================================================================
|
||||
# Validation
|
||||
# =============================================================================
|
||||
if not task_json_path.is_file():
|
||||
log_error(f"task.json not found at {task_json_path}")
|
||||
return 1
|
||||
|
||||
if adapter.requires_agent_definition_file:
|
||||
dispatch_md = adapter.get_agent_path("dispatch", project_root)
|
||||
if not dispatch_md.is_file():
|
||||
log_error(f"Agent definition not found at {dispatch_md}")
|
||||
log_info(f"Platform: {platform}")
|
||||
return 1
|
||||
|
||||
config_file = get_worktree_config(project_root)
|
||||
if not config_file.is_file():
|
||||
log_error(f"worktree.yaml not found at {config_file}")
|
||||
return 1
|
||||
|
||||
# =============================================================================
|
||||
# Read Task Config
|
||||
# =============================================================================
|
||||
print()
|
||||
print(f"{Colors.BLUE}=== Multi-Agent Pipeline: Start ==={Colors.NC}")
|
||||
log_info(f"Task: {task_dir_abs}")
|
||||
|
||||
task_data = read_json(task_json_path)
|
||||
if not task_data:
|
||||
log_error("Failed to read task.json")
|
||||
return 1
|
||||
|
||||
branch = task_data.get("branch")
|
||||
task_name = task_data.get("name")
|
||||
task_status = task_data.get("status")
|
||||
worktree_path = task_data.get("worktree_path")
|
||||
|
||||
# Check if task was rejected
|
||||
if task_status == "rejected":
|
||||
log_error("Task was rejected by Plan Agent")
|
||||
rejected_file = task_dir_abs / "REJECTED.md"
|
||||
if rejected_file.is_file():
|
||||
print()
|
||||
print(f"{Colors.YELLOW}Rejection reason:{Colors.NC}")
|
||||
print(rejected_file.read_text(encoding="utf-8"))
|
||||
print()
|
||||
log_info(
|
||||
"To retry, delete this directory and run plan.py again with revised requirements"
|
||||
)
|
||||
return 1
|
||||
|
||||
# Check if prd.md exists (plan completed successfully)
|
||||
prd_file = task_dir_abs / "prd.md"
|
||||
if not prd_file.is_file():
|
||||
log_error("prd.md not found - Plan Agent may not have completed")
|
||||
log_info(f"Check plan log: {task_dir_abs}/.plan-log")
|
||||
return 1
|
||||
|
||||
if not branch:
|
||||
log_error("branch field not set in task.json")
|
||||
log_info("Please set branch field first, e.g.:")
|
||||
log_info(
|
||||
" jq '.branch = \"task/my-task\"' task.json > tmp && mv tmp task.json"
|
||||
)
|
||||
return 1
|
||||
|
||||
log_info(f"Branch: {branch}")
|
||||
log_info(f"Name: {task_name}")
|
||||
|
||||
# =============================================================================
|
||||
# Step 1: Create Worktree (if not exists)
|
||||
# =============================================================================
|
||||
if not worktree_path or not Path(worktree_path).is_dir():
|
||||
log_info("Step 1: Creating worktree...")
|
||||
|
||||
# Record current branch as base_branch (PR target)
|
||||
_, base_branch_out, _ = run_git(
|
||||
["branch", "--show-current"], cwd=project_root
|
||||
)
|
||||
base_branch = base_branch_out.strip()
|
||||
log_info(f"Base branch (PR target): {base_branch}")
|
||||
|
||||
# Calculate worktree path
|
||||
worktree_base = get_worktree_base_dir(project_root)
|
||||
worktree_base.mkdir(parents=True, exist_ok=True)
|
||||
worktree_base = worktree_base.resolve()
|
||||
worktree_path_obj = worktree_base / branch
|
||||
worktree_path = str(worktree_path_obj)
|
||||
|
||||
# Create parent directory
|
||||
worktree_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create branch if not exists
|
||||
ret, _, _ = run_git(
|
||||
["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"],
|
||||
cwd=project_root,
|
||||
)
|
||||
if ret == 0:
|
||||
log_info("Branch exists, checking out...")
|
||||
ret, _, err = run_git(
|
||||
["worktree", "add", worktree_path, branch], cwd=project_root
|
||||
)
|
||||
else:
|
||||
log_info(f"Creating new branch: {branch}")
|
||||
ret, _, err = run_git(
|
||||
["worktree", "add", "-b", branch, worktree_path], cwd=project_root
|
||||
)
|
||||
|
||||
if ret != 0:
|
||||
log_error(f"Failed to create worktree: {err}")
|
||||
return 1
|
||||
|
||||
log_success(f"Worktree created: {worktree_path}")
|
||||
|
||||
# Update task.json with worktree_path and base_branch
|
||||
task_data["worktree_path"] = worktree_path
|
||||
task_data["base_branch"] = base_branch
|
||||
write_json(task_json_path, task_data)
|
||||
|
||||
# ----- Copy environment files -----
|
||||
log_info("Copying environment files...")
|
||||
copy_list = get_worktree_copy_files(project_root)
|
||||
copy_count = 0
|
||||
|
||||
for item in copy_list:
|
||||
if not item:
|
||||
continue
|
||||
|
||||
source = project_root / item
|
||||
target = Path(worktree_path) / item
|
||||
|
||||
if source.is_file():
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(str(source), str(target))
|
||||
copy_count += 1
|
||||
|
||||
if copy_count > 0:
|
||||
log_success(f"Copied {copy_count} file(s)")
|
||||
|
||||
# ----- Copy task directory (may not be committed yet) -----
|
||||
log_info("Copying task directory...")
|
||||
task_target_dir = Path(worktree_path) / task_dir_relative
|
||||
task_target_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
if task_target_dir.exists():
|
||||
shutil.rmtree(str(task_target_dir))
|
||||
shutil.copytree(str(task_dir_abs), str(task_target_dir))
|
||||
log_success("Task directory copied to worktree")
|
||||
|
||||
# ----- Initialize submodules (before hooks, so hooks can use submodule content) -----
|
||||
_init_submodules_for_task(task_data, worktree_path, project_root)
|
||||
|
||||
# ----- Run post_create hooks -----
|
||||
log_info("Running post_create hooks...")
|
||||
post_create = get_worktree_post_create_hooks(project_root)
|
||||
hook_count = 0
|
||||
|
||||
for cmd in post_create:
|
||||
if not cmd:
|
||||
continue
|
||||
|
||||
log_info(f" Running: {cmd}")
|
||||
ret = subprocess.run(cmd, shell=True, cwd=worktree_path)
|
||||
if ret.returncode != 0:
|
||||
log_error(f"Hook failed: {cmd}")
|
||||
return 1
|
||||
hook_count += 1
|
||||
|
||||
if hook_count > 0:
|
||||
log_success(f"Ran {hook_count} hook(s)")
|
||||
else:
|
||||
log_info(f"Step 1: Using existing worktree: {worktree_path}")
|
||||
|
||||
# ----- Initialize submodules (idempotent, for reused worktrees) -----
|
||||
_init_submodules_for_task(task_data, worktree_path, project_root)
|
||||
|
||||
# =============================================================================
|
||||
# Step 2: Set .current-task in Worktree
|
||||
# =============================================================================
|
||||
log_info("Step 2: Setting current task in worktree...")
|
||||
|
||||
worktree_workflow_dir = Path(worktree_path) / DIR_WORKFLOW
|
||||
worktree_workflow_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
current_task_file = worktree_workflow_dir / FILE_CURRENT_TASK
|
||||
current_task_file.write_text(task_dir_relative, encoding="utf-8")
|
||||
log_success(f"Current task set: {task_dir_relative}")
|
||||
|
||||
# =============================================================================
|
||||
# Step 3: Prepare and Start Claude Agent
|
||||
# =============================================================================
|
||||
log_info(f"Step 3: Starting {adapter.cli_name} agent...")
|
||||
|
||||
# Update task status
|
||||
task_data["status"] = "in_progress"
|
||||
write_json(task_json_path, task_data)
|
||||
|
||||
log_file = Path(worktree_path) / ".agent-log"
|
||||
session_id_file = Path(worktree_path) / ".session-id"
|
||||
|
||||
log_file.touch()
|
||||
|
||||
# Generate session ID for resume support (Claude Code only)
|
||||
# OpenCode generates its own session ID, we'll extract it from logs later
|
||||
if adapter.supports_session_id_on_create:
|
||||
session_id = str(uuid.uuid4()).lower()
|
||||
session_id_file.write_text(session_id, encoding="utf-8")
|
||||
log_info(f"Session ID: {session_id}")
|
||||
else:
|
||||
session_id = None # Will be extracted from logs after startup
|
||||
log_info("Session ID will be extracted from logs after startup")
|
||||
|
||||
# Get proxy environment variables
|
||||
https_proxy = os.environ.get("https_proxy", "")
|
||||
http_proxy = os.environ.get("http_proxy", "")
|
||||
all_proxy = os.environ.get("all_proxy", "")
|
||||
|
||||
# Start agent in background (cross-platform, no shell script needed)
|
||||
env = os.environ.copy()
|
||||
env["https_proxy"] = https_proxy
|
||||
env["http_proxy"] = http_proxy
|
||||
env["all_proxy"] = all_proxy
|
||||
|
||||
# Clear nested-session detection so the new CLI process can start
|
||||
# (when this script runs inside a Claude Code session, CLAUDECODE=1 is inherited)
|
||||
env.pop("CLAUDECODE", None)
|
||||
|
||||
# Set non-interactive env var based on platform
|
||||
env.update(adapter.get_non_interactive_env())
|
||||
|
||||
# Build CLI command using adapter
|
||||
# Note: Use explicit prompt to avoid confusion with CI/CD pipelines
|
||||
# Also remind the model to follow its agent definition for better cross-model compatibility
|
||||
cli_cmd = adapter.build_run_command(
|
||||
agent="dispatch",
|
||||
prompt="Follow your agent instructions to execute the task workflow. Start by reading .trellis/.current-task to get the task directory, then execute each action in task.json next_action array in order.",
|
||||
session_id=session_id if adapter.supports_session_id_on_create else None,
|
||||
skip_permissions=True,
|
||||
verbose=True,
|
||||
json_output=True,
|
||||
)
|
||||
|
||||
with log_file.open("w") as log_f:
|
||||
# Use shell=False for cross-platform compatibility
|
||||
# creationflags for Windows, start_new_session for Unix
|
||||
popen_kwargs = {
|
||||
"stdout": log_f,
|
||||
"stderr": subprocess.STDOUT,
|
||||
"cwd": worktree_path,
|
||||
"env": env,
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
else:
|
||||
popen_kwargs["start_new_session"] = True
|
||||
|
||||
process = subprocess.Popen(cli_cmd, **popen_kwargs)
|
||||
agent_pid = process.pid
|
||||
|
||||
log_success(f"Agent started with PID: {agent_pid}")
|
||||
|
||||
# For OpenCode: extract session ID from logs after startup
|
||||
if not adapter.supports_session_id_on_create:
|
||||
import time
|
||||
log_info("Waiting for session ID from logs...")
|
||||
# Wait a bit for the log to have session ID
|
||||
for _ in range(10): # Try for up to 5 seconds
|
||||
time.sleep(0.5)
|
||||
try:
|
||||
log_content = log_file.read_text(encoding="utf-8", errors="replace")
|
||||
session_id = adapter.extract_session_id_from_log(log_content)
|
||||
if session_id:
|
||||
session_id_file.write_text(session_id, encoding="utf-8")
|
||||
log_success(f"Session ID extracted: {session_id}")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
log_warn("Could not extract session ID from logs")
|
||||
session_id = "unknown"
|
||||
|
||||
# =============================================================================
|
||||
# Step 4: Register to Registry (in main repo, not worktree)
|
||||
# =============================================================================
|
||||
log_info("Step 4: Registering agent to registry...")
|
||||
|
||||
# Generate agent ID
|
||||
task_id = task_data.get("id")
|
||||
if not task_id:
|
||||
task_id = branch.replace("/", "-")
|
||||
|
||||
registry_add_agent(
|
||||
task_id, worktree_path, agent_pid, task_dir_relative, project_root, platform
|
||||
)
|
||||
|
||||
log_success(f"Agent registered: {task_id}")
|
||||
|
||||
# =============================================================================
|
||||
# Summary
|
||||
# =============================================================================
|
||||
print()
|
||||
print(f"{Colors.GREEN}=== Agent Started ==={Colors.NC}")
|
||||
print()
|
||||
print(f" ID: {task_id}")
|
||||
print(f" PID: {agent_pid}")
|
||||
print(f" Session: {session_id}")
|
||||
print(f" Worktree: {worktree_path}")
|
||||
print(f" Task: {task_dir_relative}")
|
||||
print(f" Log: {log_file}")
|
||||
print(f" Registry: {registry_get_file(project_root)}")
|
||||
print()
|
||||
print(f"{Colors.YELLOW}To monitor:{Colors.NC} tail -f {log_file}")
|
||||
print(f"{Colors.YELLOW}To stop:{Colors.NC} kill {agent_pid}")
|
||||
if session_id and session_id != "unknown":
|
||||
resume_cmd = adapter.get_resume_command_str(session_id, cwd=worktree_path)
|
||||
print(f"{Colors.YELLOW}To resume:{Colors.NC} {resume_cmd}")
|
||||
else:
|
||||
print(f"{Colors.YELLOW}To resume:{Colors.NC} (session ID not available)")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
76
.trellis/scripts/multi_agent/status.py
Executable file
76
.trellis/scripts/multi_agent/status.py
Executable file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Agent Pipeline: Status Monitor.
|
||||
|
||||
Usage:
|
||||
python3 status.py Show summary of all tasks (default)
|
||||
python3 status.py -a <assignee> Filter tasks by assignee
|
||||
python3 status.py --list List all worktrees and agents
|
||||
python3 status.py --detail <task> Detailed task status
|
||||
python3 status.py --watch <task> Watch agent log in real-time
|
||||
python3 status.py --log <task> Show recent log entries
|
||||
python3 status.py --registry Show agent registry
|
||||
|
||||
Entry shim — delegates to status_display and status_monitor.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
import _bootstrap # noqa: F401 — adds parent scripts/ dir to sys.path
|
||||
|
||||
from common.paths import get_repo_root
|
||||
|
||||
from .status_display import (
|
||||
cmd_detail,
|
||||
cmd_help,
|
||||
cmd_list,
|
||||
cmd_registry,
|
||||
cmd_summary,
|
||||
)
|
||||
from .status_monitor import cmd_log, cmd_watch
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description="Multi-Agent Pipeline: Status Monitor")
|
||||
parser.add_argument("-a", "--assignee", help="Filter by assignee")
|
||||
parser.add_argument(
|
||||
"--list", action="store_true", help="List all worktrees and agents"
|
||||
)
|
||||
parser.add_argument("--detail", metavar="TASK", help="Detailed task status")
|
||||
parser.add_argument("--progress", metavar="TASK", help="Quick progress view")
|
||||
parser.add_argument("--watch", metavar="TASK", help="Watch agent log")
|
||||
parser.add_argument("--log", metavar="TASK", help="Show recent log entries")
|
||||
parser.add_argument("--registry", action="store_true", help="Show agent registry")
|
||||
parser.add_argument("target", nargs="?", help="Target task")
|
||||
|
||||
args = parser.parse_args()
|
||||
repo_root = get_repo_root()
|
||||
|
||||
if args.list:
|
||||
return cmd_list(repo_root)
|
||||
elif args.detail:
|
||||
return cmd_detail(args.detail, repo_root)
|
||||
elif args.progress:
|
||||
return cmd_detail(args.progress, repo_root) # Similar to detail
|
||||
elif args.watch:
|
||||
return cmd_watch(args.watch, repo_root)
|
||||
elif args.log:
|
||||
return cmd_log(args.log, repo_root)
|
||||
elif args.registry:
|
||||
return cmd_registry(repo_root)
|
||||
elif args.target:
|
||||
return cmd_detail(args.target, repo_root)
|
||||
else:
|
||||
return cmd_summary(repo_root, args.assignee)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
542
.trellis/scripts/multi_agent/status_display.py
Executable file
542
.trellis/scripts/multi_agent/status_display.py
Executable file
@ -0,0 +1,542 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Agent Pipeline: Status display and formatting.
|
||||
|
||||
Provides:
|
||||
cmd_help - Show help text
|
||||
cmd_list - List worktrees and agents
|
||||
cmd_summary - Summary of all tasks with agent status
|
||||
cmd_detail - Detailed single-agent status
|
||||
cmd_registry - Dump agent registry
|
||||
|
||||
Also exports shared utilities used by status_monitor:
|
||||
is_running, find_agent, get_registry_file, calc_elapsed, count_modified_files
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from common.cli_adapter import get_cli_adapter
|
||||
from common.io import read_json
|
||||
from common.log import Colors
|
||||
from common.developer import ensure_developer
|
||||
from common.paths import (
|
||||
get_repo_root,
|
||||
get_tasks_dir,
|
||||
)
|
||||
from common.phase import get_phase_info
|
||||
from common.task_queue import format_task_stats, get_task_stats
|
||||
from common.tasks import iter_active_tasks
|
||||
from common.worktree import get_agents_dir
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Shared Utilities
|
||||
# =============================================================================
|
||||
|
||||
def is_running(pid: int | str | None) -> bool:
|
||||
"""Check if PID is running."""
|
||||
if not pid:
|
||||
return False
|
||||
try:
|
||||
pid_int = int(pid)
|
||||
os.kill(pid_int, 0)
|
||||
return True
|
||||
except (ProcessLookupError, ValueError, PermissionError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
def status_color(status: str) -> str:
|
||||
"""Get status color."""
|
||||
colors = {
|
||||
"completed": Colors.GREEN,
|
||||
"in_progress": Colors.BLUE,
|
||||
"planning": Colors.YELLOW,
|
||||
}
|
||||
return colors.get(status, Colors.DIM)
|
||||
|
||||
|
||||
def get_registry_file(repo_root: Path) -> Path | None:
|
||||
"""Get registry file path."""
|
||||
agents_dir = get_agents_dir(repo_root)
|
||||
if agents_dir:
|
||||
return agents_dir / "registry.json"
|
||||
return None
|
||||
|
||||
|
||||
def find_agent(search: str, repo_root: Path) -> dict | None:
|
||||
"""Find agent by task name or ID."""
|
||||
registry_file = get_registry_file(repo_root)
|
||||
if not registry_file or not registry_file.is_file():
|
||||
return None
|
||||
|
||||
data = read_json(registry_file)
|
||||
if not data:
|
||||
return None
|
||||
|
||||
for agent in data.get("agents", []):
|
||||
# Exact ID match
|
||||
if agent.get("id") == search:
|
||||
return agent
|
||||
# Partial match on task_dir
|
||||
task_dir = agent.get("task_dir", "")
|
||||
if search in task_dir:
|
||||
return agent
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def calc_elapsed(started: str | None) -> str:
|
||||
"""Calculate elapsed time from ISO timestamp."""
|
||||
if not started:
|
||||
return "N/A"
|
||||
|
||||
try:
|
||||
# Parse ISO format
|
||||
if "+" in started:
|
||||
started = started.split("+")[0]
|
||||
if "T" in started:
|
||||
start_dt = datetime.fromisoformat(started)
|
||||
else:
|
||||
return "N/A"
|
||||
|
||||
now = datetime.now()
|
||||
elapsed = (now - start_dt).total_seconds()
|
||||
|
||||
if elapsed < 60:
|
||||
return f"{int(elapsed)}s"
|
||||
elif elapsed < 3600:
|
||||
mins = int(elapsed // 60)
|
||||
secs = int(elapsed % 60)
|
||||
return f"{mins}m {secs}s"
|
||||
else:
|
||||
hours = int(elapsed // 3600)
|
||||
mins = int((elapsed % 3600) // 60)
|
||||
return f"{hours}h {mins}m"
|
||||
except (ValueError, TypeError):
|
||||
return "N/A"
|
||||
|
||||
|
||||
def count_modified_files(worktree: str) -> int:
|
||||
"""Count modified files in worktree."""
|
||||
if not Path(worktree).is_dir():
|
||||
return 0
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "status", "--short"],
|
||||
cwd=worktree,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
return len([line for line in result.stdout.splitlines() if line.strip()])
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Commands
|
||||
# =============================================================================
|
||||
|
||||
def cmd_help() -> int:
|
||||
"""Show help."""
|
||||
print("""Multi-Agent Pipeline: Status Monitor
|
||||
|
||||
Usage:
|
||||
python3 status.py Show summary of all tasks
|
||||
python3 status.py -a <assignee> Filter tasks by assignee
|
||||
python3 status.py --list List all worktrees and agents
|
||||
python3 status.py --detail <task> Detailed task status
|
||||
python3 status.py --progress <task> Quick progress view with recent activity
|
||||
python3 status.py --watch <task> Watch agent log in real-time
|
||||
python3 status.py --log <task> Show recent log entries
|
||||
python3 status.py --registry Show agent registry
|
||||
|
||||
Examples:
|
||||
python3 status.py -a taosu
|
||||
python3 status.py --detail my-task
|
||||
python3 status.py --progress my-task
|
||||
python3 status.py --watch 01-16-worktree-support
|
||||
python3 status.py --log worktree-support
|
||||
""")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_list(repo_root: Path) -> int:
|
||||
"""List worktrees and agents."""
|
||||
print(f"{Colors.BLUE}=== Git Worktrees ==={Colors.NC}")
|
||||
print()
|
||||
|
||||
subprocess.run(["git", "worktree", "list"], cwd=repo_root)
|
||||
print()
|
||||
|
||||
print(f"{Colors.BLUE}=== Registered Agents ==={Colors.NC}")
|
||||
print()
|
||||
|
||||
registry_file = get_registry_file(repo_root)
|
||||
if not registry_file or not registry_file.is_file():
|
||||
print(" (no registry found)")
|
||||
return 0
|
||||
|
||||
data = read_json(registry_file)
|
||||
if not data or not data.get("agents"):
|
||||
print(" (no agents registered)")
|
||||
return 0
|
||||
|
||||
for agent in data["agents"]:
|
||||
agent_id = agent.get("id", "?")
|
||||
pid = agent.get("pid")
|
||||
wt = agent.get("worktree_path", "?")
|
||||
started = agent.get("started_at", "?")
|
||||
|
||||
if is_running(pid):
|
||||
status_icon = f"{Colors.GREEN}●{Colors.NC}"
|
||||
else:
|
||||
status_icon = f"{Colors.RED}○{Colors.NC}"
|
||||
|
||||
print(f" {status_icon} {agent_id} (PID: {pid})")
|
||||
print(f" {Colors.DIM}Worktree: {wt}{Colors.NC}")
|
||||
print(f" {Colors.DIM}Started: {started}{Colors.NC}")
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_summary(repo_root: Path, filter_assignee: str | None = None) -> int:
|
||||
"""Show summary of all tasks."""
|
||||
# Import lazily to avoid circular import at module level
|
||||
from .status_monitor import get_last_tool, get_last_message
|
||||
|
||||
ensure_developer(repo_root)
|
||||
|
||||
tasks_dir = get_tasks_dir(repo_root)
|
||||
if not tasks_dir.is_dir():
|
||||
print("No tasks directory found")
|
||||
return 0
|
||||
|
||||
registry_file = get_registry_file(repo_root)
|
||||
|
||||
# Count running agents
|
||||
running_count = 0
|
||||
total_agents = 0
|
||||
|
||||
if registry_file and registry_file.is_file():
|
||||
data = read_json(registry_file)
|
||||
if data:
|
||||
agents = data.get("agents", [])
|
||||
total_agents = len(agents)
|
||||
for agent in agents:
|
||||
if is_running(agent.get("pid")):
|
||||
running_count += 1
|
||||
|
||||
# Task queue stats
|
||||
task_stats = get_task_stats(repo_root)
|
||||
|
||||
print(f"{Colors.BLUE}=== Multi-Agent Status ==={Colors.NC}")
|
||||
print(
|
||||
f" Agents: {Colors.GREEN}{running_count}{Colors.NC} running / {total_agents} registered"
|
||||
)
|
||||
print(f" Tasks: {format_task_stats(task_stats)}")
|
||||
print()
|
||||
|
||||
# Process tasks
|
||||
running_tasks = []
|
||||
stopped_tasks = []
|
||||
regular_tasks = []
|
||||
|
||||
registry_data = (
|
||||
read_json(registry_file)
|
||||
if registry_file and registry_file.is_file()
|
||||
else None
|
||||
)
|
||||
|
||||
for t in iter_active_tasks(tasks_dir):
|
||||
name = t.dir_name
|
||||
status = t.status
|
||||
assignee = t.assignee or "unassigned"
|
||||
priority = t.priority
|
||||
|
||||
# Filter by assignee
|
||||
if filter_assignee and assignee != filter_assignee:
|
||||
continue
|
||||
|
||||
# Check agent status
|
||||
agent_info = None
|
||||
if registry_data:
|
||||
for agent in registry_data.get("agents", []):
|
||||
if name in agent.get("task_dir", ""):
|
||||
agent_info = agent
|
||||
break
|
||||
|
||||
if agent_info:
|
||||
pid = agent_info.get("pid")
|
||||
worktree = agent_info.get("worktree_path", "")
|
||||
started = agent_info.get("started_at")
|
||||
agent_platform = agent_info.get("platform", "claude")
|
||||
|
||||
if is_running(pid):
|
||||
# Running agent
|
||||
task_dir_rel = agent_info.get("task_dir", "")
|
||||
worktree_task_json = Path(worktree) / task_dir_rel / "task.json"
|
||||
phase_source = t.directory / "task.json"
|
||||
if worktree_task_json.is_file():
|
||||
phase_source = worktree_task_json
|
||||
|
||||
phase_info_str = get_phase_info(phase_source)
|
||||
elapsed = calc_elapsed(started)
|
||||
modified = count_modified_files(worktree)
|
||||
|
||||
worktree_data = read_json(phase_source)
|
||||
branch = worktree_data.get("branch", "N/A") if worktree_data else "N/A"
|
||||
|
||||
log_file = Path(worktree) / ".agent-log"
|
||||
last_tool = get_last_tool(log_file, platform=agent_platform)
|
||||
|
||||
running_tasks.append(
|
||||
{
|
||||
"name": name,
|
||||
"priority": priority,
|
||||
"assignee": assignee,
|
||||
"phase_info": phase_info_str,
|
||||
"elapsed": elapsed,
|
||||
"branch": branch,
|
||||
"modified": modified,
|
||||
"last_tool": last_tool,
|
||||
"pid": pid,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Stopped agent
|
||||
task_dir_rel = agent_info.get("task_dir", "")
|
||||
worktree_task_json = Path(worktree) / task_dir_rel / "task.json"
|
||||
worktree_status = "unknown"
|
||||
|
||||
if worktree_task_json.is_file():
|
||||
wt_data = read_json(worktree_task_json)
|
||||
if wt_data:
|
||||
worktree_status = wt_data.get("status", "unknown")
|
||||
|
||||
session_id_file = Path(worktree) / ".session-id"
|
||||
log_file = Path(worktree) / ".agent-log"
|
||||
|
||||
stopped_tasks.append(
|
||||
{
|
||||
"name": name,
|
||||
"worktree": worktree,
|
||||
"status": worktree_status,
|
||||
"session_id_file": session_id_file,
|
||||
"log_file": log_file,
|
||||
"platform": agent_info.get("platform", "claude"),
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Regular task
|
||||
regular_tasks.append(
|
||||
{
|
||||
"name": name,
|
||||
"status": status,
|
||||
"priority": priority,
|
||||
"assignee": assignee,
|
||||
}
|
||||
)
|
||||
|
||||
# Output running agents
|
||||
if running_tasks:
|
||||
print(f"{Colors.CYAN}Running Agents:{Colors.NC}")
|
||||
for t in running_tasks:
|
||||
priority_color = (
|
||||
Colors.RED
|
||||
if t["priority"] == "P0"
|
||||
else (Colors.YELLOW if t["priority"] == "P1" else Colors.BLUE)
|
||||
)
|
||||
print(
|
||||
f"{Colors.GREEN}▶{Colors.NC} {Colors.CYAN}{t['name']}{Colors.NC} {Colors.GREEN}[running]{Colors.NC} {priority_color}[{t['priority']}]{Colors.NC} @{t['assignee']}"
|
||||
)
|
||||
print(f" Phase: {t['phase_info']}")
|
||||
print(f" Elapsed: {t['elapsed']}")
|
||||
print(f" Branch: {Colors.DIM}{t['branch']}{Colors.NC}")
|
||||
print(f" Modified: {t['modified']} file(s)")
|
||||
if t["last_tool"]:
|
||||
print(f" Activity: {Colors.YELLOW}{t['last_tool']}{Colors.NC}")
|
||||
print(f" PID: {Colors.DIM}{t['pid']}{Colors.NC}")
|
||||
print()
|
||||
|
||||
# Output stopped agents
|
||||
if stopped_tasks:
|
||||
print(f"{Colors.RED}Stopped Agents:{Colors.NC}")
|
||||
for t in stopped_tasks:
|
||||
if t["status"] == "completed":
|
||||
print(
|
||||
f"{Colors.GREEN}✓{Colors.NC} {t['name']} {Colors.GREEN}[completed]{Colors.NC}"
|
||||
)
|
||||
else:
|
||||
if t["session_id_file"].is_file():
|
||||
session_id = (
|
||||
t["session_id_file"].read_text(encoding="utf-8").strip()
|
||||
)
|
||||
last_msg = get_last_message(t["log_file"], 150, platform=t.get("platform", "claude"))
|
||||
print(
|
||||
f"{Colors.RED}○{Colors.NC} {t['name']} {Colors.RED}[stopped]{Colors.NC}"
|
||||
)
|
||||
if last_msg:
|
||||
print(f'{Colors.DIM}"{last_msg}"{Colors.NC}')
|
||||
# Use CLI adapter for platform-specific resume command
|
||||
adapter = get_cli_adapter(t.get("platform", "claude"))
|
||||
resume_cmd = adapter.get_resume_command_str(session_id, cwd=t["worktree"])
|
||||
print(f"{Colors.YELLOW}{resume_cmd}{Colors.NC}")
|
||||
else:
|
||||
print(
|
||||
f"{Colors.RED}○{Colors.NC} {t['name']} {Colors.RED}[stopped]{Colors.NC} {Colors.DIM}(no session-id){Colors.NC}"
|
||||
)
|
||||
print()
|
||||
|
||||
# Separator
|
||||
if (running_tasks or stopped_tasks) and regular_tasks:
|
||||
print(f"{Colors.DIM}───────────────────────────────────────{Colors.NC}")
|
||||
print()
|
||||
|
||||
# Output regular tasks grouped by assignee
|
||||
if regular_tasks:
|
||||
# Sort by assignee, priority, status
|
||||
regular_tasks.sort(
|
||||
key=lambda x: (
|
||||
x["assignee"],
|
||||
{"P0": 0, "P1": 1, "P2": 2, "P3": 3}.get(x["priority"], 2),
|
||||
{"in_progress": 0, "planning": 1, "completed": 2}.get(x["status"], 1),
|
||||
)
|
||||
)
|
||||
|
||||
current_assignee = None
|
||||
for t in regular_tasks:
|
||||
if t["assignee"] != current_assignee:
|
||||
if current_assignee is not None:
|
||||
print()
|
||||
print(f"{Colors.CYAN}@{t['assignee']}:{Colors.NC}")
|
||||
current_assignee = t["assignee"]
|
||||
|
||||
color = status_color(t["status"])
|
||||
priority_color = (
|
||||
Colors.RED
|
||||
if t["priority"] == "P0"
|
||||
else (Colors.YELLOW if t["priority"] == "P1" else Colors.BLUE)
|
||||
)
|
||||
print(
|
||||
f" {color}●{Colors.NC} {t['name']} ({t['status']}) {priority_color}[{t['priority']}]{Colors.NC}"
|
||||
)
|
||||
|
||||
if running_tasks:
|
||||
print()
|
||||
print(f"{Colors.DIM}─────────────────────────────────────{Colors.NC}")
|
||||
print(f"{Colors.DIM}Use --progress <name> for quick activity view{Colors.NC}")
|
||||
print(f"{Colors.DIM}Use --detail <name> for more info{Colors.NC}")
|
||||
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_detail(target: str, repo_root: Path) -> int:
|
||||
"""Show detailed task status."""
|
||||
agent = find_agent(target, repo_root)
|
||||
if not agent:
|
||||
print(f"Agent not found: {target}")
|
||||
return 1
|
||||
|
||||
agent_id = agent.get("id", "?")
|
||||
pid = agent.get("pid")
|
||||
worktree = agent.get("worktree_path", "?")
|
||||
task_dir = agent.get("task_dir", "?")
|
||||
started = agent.get("started_at", "?")
|
||||
platform = agent.get("platform", "claude")
|
||||
|
||||
# Check for session-id
|
||||
session_id = ""
|
||||
session_id_file = Path(worktree) / ".session-id"
|
||||
if session_id_file.is_file():
|
||||
session_id = session_id_file.read_text(encoding="utf-8").strip()
|
||||
|
||||
print(f"{Colors.BLUE}=== Agent Detail: {agent_id} ==={Colors.NC}")
|
||||
print()
|
||||
print(f" ID: {agent_id}")
|
||||
print(f" PID: {pid}")
|
||||
print(f" Session: {session_id or 'N/A'}")
|
||||
print(f" Worktree: {worktree}")
|
||||
print(f" Task Dir: {task_dir}")
|
||||
print(f" Started: {started}")
|
||||
print()
|
||||
|
||||
# Status
|
||||
if is_running(pid):
|
||||
print(f" Status: {Colors.GREEN}Running{Colors.NC}")
|
||||
else:
|
||||
print(f" Status: {Colors.RED}Stopped{Colors.NC}")
|
||||
if session_id:
|
||||
print()
|
||||
# Use CLI adapter for platform-specific resume command
|
||||
adapter = get_cli_adapter(platform)
|
||||
resume_cmd = adapter.get_resume_command_str(session_id, cwd=worktree)
|
||||
print(f" {Colors.YELLOW}Resume:{Colors.NC} {resume_cmd}")
|
||||
|
||||
# Task info
|
||||
task_json = repo_root / task_dir / "task.json"
|
||||
if task_json.is_file():
|
||||
print()
|
||||
print(f"{Colors.BLUE}=== Task Info ==={Colors.NC}")
|
||||
print()
|
||||
data = read_json(task_json)
|
||||
if data:
|
||||
print(f" Status: {data.get('status', 'unknown')}")
|
||||
print(f" Branch: {data.get('branch', 'N/A')}")
|
||||
print(f" Base Branch: {data.get('base_branch', 'N/A')}")
|
||||
|
||||
# Git changes
|
||||
if Path(worktree).is_dir():
|
||||
print()
|
||||
print(f"{Colors.BLUE}=== Git Changes ==={Colors.NC}")
|
||||
print()
|
||||
|
||||
result = subprocess.run(
|
||||
["git", "status", "--short"],
|
||||
cwd=worktree,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
changes = result.stdout.strip()
|
||||
if changes:
|
||||
for line in changes.splitlines()[:10]:
|
||||
print(f" {line}")
|
||||
total = len(changes.splitlines())
|
||||
if total > 10:
|
||||
print(f" ... and {total - 10} more")
|
||||
else:
|
||||
print(" (no changes)")
|
||||
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_registry(repo_root: Path) -> int:
|
||||
"""Show agent registry."""
|
||||
registry_file = get_registry_file(repo_root)
|
||||
|
||||
print(f"{Colors.BLUE}=== Agent Registry ==={Colors.NC}")
|
||||
print()
|
||||
print(f"File: {registry_file}")
|
||||
print()
|
||||
|
||||
if registry_file and registry_file.is_file():
|
||||
data = read_json(registry_file)
|
||||
if data:
|
||||
print(json.dumps(data, indent=2))
|
||||
else:
|
||||
print("(registry not found)")
|
||||
|
||||
return 0
|
||||
225
.trellis/scripts/multi_agent/status_monitor.py
Executable file
225
.trellis/scripts/multi_agent/status_monitor.py
Executable file
@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Agent Pipeline: Process monitoring and log parsing.
|
||||
|
||||
Provides:
|
||||
tail_follow - Follow a file like 'tail -f'
|
||||
get_last_tool - Get last tool call from agent log
|
||||
get_last_message - Get last assistant text from agent log
|
||||
cmd_watch - Watch agent log in real-time
|
||||
cmd_log - Show recent log entries
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from common.log import Colors
|
||||
|
||||
from .status_display import find_agent
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Log Parsing
|
||||
# =============================================================================
|
||||
|
||||
def tail_follow(file_path: Path) -> None:
|
||||
"""Follow a file like 'tail -f', cross-platform compatible."""
|
||||
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
||||
# Seek to end of file
|
||||
f.seek(0, 2)
|
||||
|
||||
while True:
|
||||
line = f.readline()
|
||||
if line:
|
||||
print(line, end="", flush=True)
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def get_last_tool(log_file: Path, platform: str = "claude") -> str | None:
|
||||
"""Get the last tool call from agent log.
|
||||
|
||||
Supports both Claude Code and OpenCode log formats.
|
||||
|
||||
Claude Code format:
|
||||
{"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Read"}]}}
|
||||
|
||||
OpenCode format:
|
||||
{"type": "tool_use", "tool": "bash", "state": {"status": "completed"}}
|
||||
"""
|
||||
if not log_file.is_file():
|
||||
return None
|
||||
|
||||
try:
|
||||
lines = log_file.read_text(encoding="utf-8").splitlines()
|
||||
for line in reversed(lines[-100:]):
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
if platform == "opencode":
|
||||
# OpenCode format: {"type": "tool_use", "tool": "bash", ...}
|
||||
if data.get("type") == "tool_use":
|
||||
return data.get("tool")
|
||||
else:
|
||||
# Claude Code format: {"type": "assistant", "message": {"content": [...]}}
|
||||
if data.get("type") == "assistant":
|
||||
content = data.get("message", {}).get("content", [])
|
||||
for item in content:
|
||||
if item.get("type") == "tool_use":
|
||||
return item.get("name")
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def get_last_message(log_file: Path, max_len: int = 100, platform: str = "claude") -> str | None:
|
||||
"""Get the last assistant text from agent log.
|
||||
|
||||
Supports both Claude Code and OpenCode log formats.
|
||||
|
||||
Claude Code format:
|
||||
{"type": "assistant", "message": {"content": [{"type": "text", "text": "..."}]}}
|
||||
|
||||
OpenCode format:
|
||||
{"type": "text", "text": "..."}
|
||||
"""
|
||||
if not log_file.is_file():
|
||||
return None
|
||||
|
||||
try:
|
||||
lines = log_file.read_text(encoding="utf-8").splitlines()
|
||||
for line in reversed(lines[-100:]):
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
if platform == "opencode":
|
||||
# OpenCode format: {"type": "text", "text": "..."}
|
||||
if data.get("type") == "text":
|
||||
text = data.get("text", "")
|
||||
if text:
|
||||
return text[:max_len]
|
||||
else:
|
||||
# Claude Code format: {"type": "assistant", "message": {"content": [...]}}
|
||||
if data.get("type") == "assistant":
|
||||
content = data.get("message", {}).get("content", [])
|
||||
for item in content:
|
||||
if item.get("type") == "text":
|
||||
text = item.get("text", "")
|
||||
if text:
|
||||
return text[:max_len]
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Commands
|
||||
# =============================================================================
|
||||
|
||||
def cmd_watch(target: str, repo_root: Path) -> int:
|
||||
"""Watch agent log in real-time."""
|
||||
agent = find_agent(target, repo_root)
|
||||
if not agent:
|
||||
print(f"Agent not found: {target}")
|
||||
return 1
|
||||
|
||||
worktree = agent.get("worktree_path", "")
|
||||
log_file = Path(worktree) / ".agent-log"
|
||||
|
||||
if not log_file.is_file():
|
||||
print(f"Log file not found: {log_file}")
|
||||
return 1
|
||||
|
||||
print(f"{Colors.BLUE}Watching:{Colors.NC} {log_file}")
|
||||
print(f"{Colors.DIM}Press Ctrl+C to stop{Colors.NC}")
|
||||
print()
|
||||
|
||||
try:
|
||||
tail_follow(log_file)
|
||||
except KeyboardInterrupt:
|
||||
print() # Clean newline after Ctrl+C
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_log(target: str, repo_root: Path) -> int:
|
||||
"""Show recent log entries."""
|
||||
agent = find_agent(target, repo_root)
|
||||
if not agent:
|
||||
print(f"Agent not found: {target}")
|
||||
return 1
|
||||
|
||||
worktree = agent.get("worktree_path", "")
|
||||
platform = agent.get("platform", "claude")
|
||||
log_file = Path(worktree) / ".agent-log"
|
||||
|
||||
if not log_file.is_file():
|
||||
print(f"Log file not found: {log_file}")
|
||||
return 1
|
||||
|
||||
print(f"{Colors.BLUE}=== Recent Log: {target} ==={Colors.NC}")
|
||||
print(f"{Colors.DIM}Platform: {platform}{Colors.NC}")
|
||||
print()
|
||||
|
||||
lines = log_file.read_text(encoding="utf-8").splitlines()
|
||||
for line in lines[-50:]:
|
||||
try:
|
||||
data = json.loads(line)
|
||||
msg_type = data.get("type", "")
|
||||
|
||||
if platform == "opencode":
|
||||
# OpenCode format
|
||||
if msg_type == "text":
|
||||
text = data.get("text", "")
|
||||
if text:
|
||||
display = text[:300]
|
||||
if len(text) > 300:
|
||||
display += "..."
|
||||
print(f"{Colors.BLUE}[TEXT]{Colors.NC} {display}")
|
||||
elif msg_type == "tool_use":
|
||||
tool_name = data.get("tool", "unknown")
|
||||
status = data.get("state", {}).get("status", "")
|
||||
print(f"{Colors.YELLOW}[TOOL]{Colors.NC} {tool_name} ({status})")
|
||||
elif msg_type == "step_start":
|
||||
print(f"{Colors.CYAN}[STEP]{Colors.NC} Start")
|
||||
elif msg_type == "step_finish":
|
||||
reason = data.get("reason", "")
|
||||
print(f"{Colors.CYAN}[STEP]{Colors.NC} Finish ({reason})")
|
||||
elif msg_type == "error":
|
||||
error_msg = data.get("message", "")
|
||||
print(f"{Colors.RED}[ERROR]{Colors.NC} {error_msg}")
|
||||
else:
|
||||
# Claude Code format
|
||||
if msg_type == "system":
|
||||
subtype = data.get("subtype", "")
|
||||
print(f"{Colors.CYAN}[SYSTEM]{Colors.NC} {subtype}")
|
||||
elif msg_type == "user":
|
||||
content = data.get("message", {}).get("content", "")
|
||||
if content:
|
||||
print(f"{Colors.GREEN}[USER]{Colors.NC} {content[:200]}")
|
||||
elif msg_type == "assistant":
|
||||
content = data.get("message", {}).get("content", [])
|
||||
if content:
|
||||
item = content[0]
|
||||
text = item.get("text")
|
||||
tool = item.get("name")
|
||||
if text:
|
||||
display = text[:300]
|
||||
if len(text) > 300:
|
||||
display += "..."
|
||||
print(f"{Colors.BLUE}[ASSISTANT]{Colors.NC} {display}")
|
||||
elif tool:
|
||||
print(f"{Colors.YELLOW}[TOOL]{Colors.NC} {tool}")
|
||||
elif msg_type == "result":
|
||||
tool_name = data.get("tool", "unknown")
|
||||
print(f"{Colors.DIM}[RESULT]{Colors.NC} {tool_name} completed")
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return 0
|
||||
445
.trellis/scripts/task.py
Executable file
445
.trellis/scripts/task.py
Executable file
@ -0,0 +1,445 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Task Management Script for Multi-Agent Pipeline.
|
||||
|
||||
Usage:
|
||||
python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>]
|
||||
python3 task.py init-context <dir> <type> [--package <pkg>] # Initialize jsonl files
|
||||
python3 task.py add-context <dir> <file> <path> [reason] # Add jsonl entry
|
||||
python3 task.py validate <dir> # Validate jsonl files
|
||||
python3 task.py list-context <dir> # List jsonl entries
|
||||
python3 task.py start <dir> # Set as current task
|
||||
python3 task.py finish # Clear current task
|
||||
python3 task.py set-branch <dir> <branch> # Set git branch
|
||||
python3 task.py set-base-branch <dir> <branch> # Set PR target branch
|
||||
python3 task.py set-scope <dir> <scope> # Set scope for PR title
|
||||
python3 task.py create-pr [dir] [--dry-run] # Create PR from task
|
||||
python3 task.py archive <task-name> # Archive completed task
|
||||
python3 task.py list # List active tasks
|
||||
python3 task.py list-archive [month] # List archived tasks
|
||||
python3 task.py add-subtask <parent-dir> <child-dir> # Link child to parent
|
||||
python3 task.py remove-subtask <parent-dir> <child-dir> # Unlink child from parent
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from common.log import Colors, colored
|
||||
from common.paths import (
|
||||
DIR_WORKFLOW,
|
||||
DIR_TASKS,
|
||||
FILE_TASK_JSON,
|
||||
get_repo_root,
|
||||
get_developer,
|
||||
get_tasks_dir,
|
||||
get_current_task,
|
||||
set_current_task,
|
||||
clear_current_task,
|
||||
)
|
||||
from common.task_utils import resolve_task_dir, run_task_hooks
|
||||
from common.tasks import iter_active_tasks, children_progress
|
||||
|
||||
# Import command handlers from split modules (also re-exports for plan.py compatibility)
|
||||
from common.task_store import (
|
||||
cmd_create,
|
||||
cmd_archive,
|
||||
cmd_set_branch,
|
||||
cmd_set_base_branch,
|
||||
cmd_set_scope,
|
||||
cmd_add_subtask,
|
||||
cmd_remove_subtask,
|
||||
)
|
||||
from common.task_context import (
|
||||
cmd_init_context,
|
||||
cmd_add_context,
|
||||
cmd_validate,
|
||||
cmd_list_context,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Command: start / finish
|
||||
# =============================================================================
|
||||
|
||||
def cmd_start(args: argparse.Namespace) -> int:
|
||||
"""Set current task."""
|
||||
repo_root = get_repo_root()
|
||||
task_input = args.dir
|
||||
|
||||
if not task_input:
|
||||
print(colored("Error: task directory or name required", Colors.RED))
|
||||
return 1
|
||||
|
||||
# Resolve task directory (supports task name, relative path, or absolute path)
|
||||
full_path = resolve_task_dir(task_input, repo_root)
|
||||
|
||||
if not full_path.is_dir():
|
||||
print(colored(f"Error: Task not found: {task_input}", Colors.RED))
|
||||
print("Hint: Use task name (e.g., 'my-task') or full path (e.g., '.trellis/tasks/01-31-my-task')")
|
||||
return 1
|
||||
|
||||
# Convert to relative path for storage
|
||||
try:
|
||||
task_dir = full_path.relative_to(repo_root).as_posix()
|
||||
except ValueError:
|
||||
task_dir = str(full_path)
|
||||
|
||||
if set_current_task(task_dir, repo_root):
|
||||
print(colored(f"✓ Current task set to: {task_dir}", Colors.GREEN))
|
||||
print()
|
||||
print(colored("The hook will now inject context from this task's jsonl files.", Colors.BLUE))
|
||||
|
||||
task_json_path = full_path / FILE_TASK_JSON
|
||||
run_task_hooks("after_start", task_json_path, repo_root)
|
||||
return 0
|
||||
else:
|
||||
print(colored("Error: Failed to set current task", Colors.RED))
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_finish(args: argparse.Namespace) -> int:
|
||||
"""Clear current task."""
|
||||
repo_root = get_repo_root()
|
||||
current = get_current_task(repo_root)
|
||||
|
||||
if not current:
|
||||
print(colored("No current task set", Colors.YELLOW))
|
||||
return 0
|
||||
|
||||
# Resolve task.json path before clearing
|
||||
task_json_path = repo_root / current / FILE_TASK_JSON
|
||||
|
||||
clear_current_task(repo_root)
|
||||
print(colored(f"✓ Cleared current task (was: {current})", Colors.GREEN))
|
||||
|
||||
if task_json_path.is_file():
|
||||
run_task_hooks("after_finish", task_json_path, repo_root)
|
||||
return 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Command: list
|
||||
# =============================================================================
|
||||
|
||||
def cmd_list(args: argparse.Namespace) -> int:
|
||||
"""List active tasks."""
|
||||
repo_root = get_repo_root()
|
||||
tasks_dir = get_tasks_dir(repo_root)
|
||||
current_task = get_current_task(repo_root)
|
||||
developer = get_developer(repo_root)
|
||||
filter_mine = args.mine
|
||||
filter_status = args.status
|
||||
|
||||
if filter_mine:
|
||||
if not developer:
|
||||
print(colored("Error: No developer set. Run init_developer.py first", Colors.RED), file=sys.stderr)
|
||||
return 1
|
||||
print(colored(f"My tasks (assignee: {developer}):", Colors.BLUE))
|
||||
else:
|
||||
print(colored("All active tasks:", Colors.BLUE))
|
||||
print()
|
||||
|
||||
# Single pass: collect all tasks via shared iterator
|
||||
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()}
|
||||
|
||||
# Display tasks hierarchically
|
||||
count = 0
|
||||
|
||||
def _print_task(dir_name: str, indent: int = 0) -> None:
|
||||
nonlocal count
|
||||
t = all_tasks[dir_name]
|
||||
|
||||
# Apply --mine filter
|
||||
if filter_mine and (t.assignee or "-") != developer:
|
||||
return
|
||||
|
||||
# Apply --status filter
|
||||
if filter_status and t.status != filter_status:
|
||||
return
|
||||
|
||||
relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}"
|
||||
marker = ""
|
||||
if relative_path == current_task:
|
||||
marker = f" {colored('<- current', Colors.GREEN)}"
|
||||
|
||||
# Children progress
|
||||
progress = children_progress(t.children, all_statuses)
|
||||
|
||||
# Package tag
|
||||
pkg_tag = f" @{t.package}" if t.package else ""
|
||||
|
||||
prefix = " " * indent + " - "
|
||||
|
||||
if filter_mine:
|
||||
print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress}{marker}")
|
||||
else:
|
||||
print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}")
|
||||
count += 1
|
||||
|
||||
# Print children indented
|
||||
for child_name in t.children:
|
||||
if child_name in all_tasks:
|
||||
_print_task(child_name, indent + 1)
|
||||
|
||||
# Display only top-level tasks (those without a parent)
|
||||
for dir_name in sorted(all_tasks.keys()):
|
||||
if not all_tasks[dir_name].parent:
|
||||
_print_task(dir_name)
|
||||
|
||||
if count == 0:
|
||||
if filter_mine:
|
||||
print(" (no tasks assigned to you)")
|
||||
else:
|
||||
print(" (no active tasks)")
|
||||
|
||||
print()
|
||||
print(f"Total: {count} task(s)")
|
||||
return 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Command: list-archive
|
||||
# =============================================================================
|
||||
|
||||
def cmd_list_archive(args: argparse.Namespace) -> int:
|
||||
"""List archived tasks."""
|
||||
repo_root = get_repo_root()
|
||||
tasks_dir = get_tasks_dir(repo_root)
|
||||
archive_dir = tasks_dir / "archive"
|
||||
month = args.month
|
||||
|
||||
print(colored("Archived tasks:", Colors.BLUE))
|
||||
print()
|
||||
|
||||
if month:
|
||||
month_dir = archive_dir / month
|
||||
if month_dir.is_dir():
|
||||
print(f"[{month}]")
|
||||
for d in sorted(month_dir.iterdir()):
|
||||
if d.is_dir():
|
||||
print(f" - {d.name}/")
|
||||
else:
|
||||
print(f" No archives for {month}")
|
||||
else:
|
||||
if archive_dir.is_dir():
|
||||
for month_dir in sorted(archive_dir.iterdir()):
|
||||
if month_dir.is_dir():
|
||||
month_name = month_dir.name
|
||||
count = sum(1 for d in month_dir.iterdir() if d.is_dir())
|
||||
print(f"[{month_name}] - {count} task(s)")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Command: create-pr (delegates to multi-agent script)
|
||||
# =============================================================================
|
||||
|
||||
def cmd_create_pr(args: argparse.Namespace) -> int:
|
||||
"""Create PR from task - delegates to multi_agent/create_pr.py."""
|
||||
import subprocess
|
||||
script_dir = Path(__file__).parent
|
||||
create_pr_script = script_dir / "multi_agent" / "create_pr.py"
|
||||
|
||||
cmd = [sys.executable, str(create_pr_script)]
|
||||
if args.dir:
|
||||
cmd.append(args.dir)
|
||||
if args.dry_run:
|
||||
cmd.append("--dry-run")
|
||||
|
||||
result = subprocess.run(cmd)
|
||||
return result.returncode
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Help
|
||||
# =============================================================================
|
||||
|
||||
def show_usage() -> None:
|
||||
"""Show usage help."""
|
||||
print("""Task Management Script for Multi-Agent Pipeline
|
||||
|
||||
Usage:
|
||||
python3 task.py create <title> Create new task directory
|
||||
python3 task.py create <title> --package <pkg> Create task for a specific package
|
||||
python3 task.py create <title> --parent <dir> Create task as child of parent
|
||||
python3 task.py init-context <dir> <dev_type> Initialize jsonl files
|
||||
python3 task.py init-context <dir> <type> --package <pkg> With explicit package
|
||||
python3 task.py add-context <dir> <jsonl> <path> [reason] Add entry to jsonl
|
||||
python3 task.py validate <dir> Validate jsonl files
|
||||
python3 task.py list-context <dir> List jsonl entries
|
||||
python3 task.py start <dir> Set as current task
|
||||
python3 task.py finish Clear current task
|
||||
python3 task.py set-branch <dir> <branch> Set git branch for multi-agent
|
||||
python3 task.py set-scope <dir> <scope> Set scope for PR title
|
||||
python3 task.py create-pr [dir] [--dry-run] Create PR from task
|
||||
python3 task.py archive <task-name> Archive completed task
|
||||
python3 task.py add-subtask <parent> <child> Link child task to parent
|
||||
python3 task.py remove-subtask <parent> <child> Unlink child from parent
|
||||
python3 task.py list [--mine] [--status <status>] List tasks
|
||||
python3 task.py list-archive [YYYY-MM] List archived tasks
|
||||
|
||||
Arguments:
|
||||
dev_type: backend | frontend | fullstack | test | docs
|
||||
|
||||
Monorepo options:
|
||||
--package <pkg> Package name (validated against config.yaml packages)
|
||||
|
||||
List options:
|
||||
--mine, -m Show only tasks assigned to current developer
|
||||
--status, -s <s> Filter by status (planning, in_progress, review, completed)
|
||||
|
||||
Examples:
|
||||
python3 task.py create "Add login feature" --slug add-login
|
||||
python3 task.py create "Add login feature" --slug add-login --package cli
|
||||
python3 task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent
|
||||
python3 task.py init-context .trellis/tasks/01-21-add-login backend
|
||||
python3 task.py init-context .trellis/tasks/01-21-add-login backend --package cli
|
||||
python3 task.py add-context <dir> implement .trellis/spec/cli/backend/auth.md "Auth guidelines"
|
||||
python3 task.py set-branch <dir> task/add-login
|
||||
python3 task.py start .trellis/tasks/01-21-add-login
|
||||
python3 task.py create-pr # Uses current task
|
||||
python3 task.py create-pr <dir> --dry-run # Preview without changes
|
||||
python3 task.py finish
|
||||
python3 task.py archive add-login
|
||||
python3 task.py add-subtask parent-task child-task # Link existing tasks
|
||||
python3 task.py remove-subtask parent-task child-task
|
||||
python3 task.py list # List all active tasks
|
||||
python3 task.py list --mine # List my tasks only
|
||||
python3 task.py list --mine --status in_progress # List my in-progress tasks
|
||||
""")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main Entry
|
||||
# =============================================================================
|
||||
|
||||
def main() -> int:
|
||||
"""CLI entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Task Management Script for Multi-Agent Pipeline",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
||||
# create
|
||||
p_create = subparsers.add_parser("create", help="Create new task")
|
||||
p_create.add_argument("title", help="Task title")
|
||||
p_create.add_argument("--slug", "-s", help="Task slug")
|
||||
p_create.add_argument("--assignee", "-a", help="Assignee developer")
|
||||
p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)")
|
||||
p_create.add_argument("--description", "-d", help="Task description")
|
||||
p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
|
||||
p_create.add_argument("--package", help="Package name for monorepo projects")
|
||||
|
||||
# init-context
|
||||
p_init = subparsers.add_parser("init-context", help="Initialize context files")
|
||||
p_init.add_argument("dir", help="Task directory")
|
||||
p_init.add_argument("type", help="Dev type: backend|frontend|fullstack|test|docs")
|
||||
p_init.add_argument("--package", help="Package name for monorepo projects")
|
||||
|
||||
# add-context
|
||||
p_add = subparsers.add_parser("add-context", help="Add context entry")
|
||||
p_add.add_argument("dir", help="Task directory")
|
||||
p_add.add_argument("file", help="JSONL file (implement|check|debug)")
|
||||
p_add.add_argument("path", help="File path to add")
|
||||
p_add.add_argument("reason", nargs="?", help="Reason for adding")
|
||||
|
||||
# validate
|
||||
p_validate = subparsers.add_parser("validate", help="Validate context files")
|
||||
p_validate.add_argument("dir", help="Task directory")
|
||||
|
||||
# list-context
|
||||
p_listctx = subparsers.add_parser("list-context", help="List context entries")
|
||||
p_listctx.add_argument("dir", help="Task directory")
|
||||
|
||||
# start
|
||||
p_start = subparsers.add_parser("start", help="Set current task")
|
||||
p_start.add_argument("dir", help="Task directory")
|
||||
|
||||
# finish
|
||||
subparsers.add_parser("finish", help="Clear current task")
|
||||
|
||||
# set-branch
|
||||
p_branch = subparsers.add_parser("set-branch", help="Set git branch")
|
||||
p_branch.add_argument("dir", help="Task directory")
|
||||
p_branch.add_argument("branch", help="Branch name")
|
||||
|
||||
# set-base-branch
|
||||
p_base = subparsers.add_parser("set-base-branch", help="Set PR target branch")
|
||||
p_base.add_argument("dir", help="Task directory")
|
||||
p_base.add_argument("base_branch", help="Base branch name (PR target)")
|
||||
|
||||
# set-scope
|
||||
p_scope = subparsers.add_parser("set-scope", help="Set scope")
|
||||
p_scope.add_argument("dir", help="Task directory")
|
||||
p_scope.add_argument("scope", help="Scope name")
|
||||
|
||||
# create-pr
|
||||
p_pr = subparsers.add_parser("create-pr", help="Create PR")
|
||||
p_pr.add_argument("dir", nargs="?", help="Task directory")
|
||||
p_pr.add_argument("--dry-run", action="store_true", help="Dry run mode")
|
||||
|
||||
# archive
|
||||
p_archive = subparsers.add_parser("archive", help="Archive task")
|
||||
p_archive.add_argument("name", help="Task name")
|
||||
p_archive.add_argument("--no-commit", action="store_true", help="Skip auto git commit after archive")
|
||||
|
||||
# list
|
||||
p_list = subparsers.add_parser("list", help="List tasks")
|
||||
p_list.add_argument("--mine", "-m", action="store_true", help="My tasks only")
|
||||
p_list.add_argument("--status", "-s", help="Filter by status")
|
||||
|
||||
# add-subtask
|
||||
p_addsub = subparsers.add_parser("add-subtask", help="Link child task to parent")
|
||||
p_addsub.add_argument("parent_dir", help="Parent task directory")
|
||||
p_addsub.add_argument("child_dir", help="Child task directory")
|
||||
|
||||
# remove-subtask
|
||||
p_rmsub = subparsers.add_parser("remove-subtask", help="Unlink child task from parent")
|
||||
p_rmsub.add_argument("parent_dir", help="Parent task directory")
|
||||
p_rmsub.add_argument("child_dir", help="Child task directory")
|
||||
|
||||
# list-archive
|
||||
p_listarch = subparsers.add_parser("list-archive", help="List archived tasks")
|
||||
p_listarch.add_argument("month", nargs="?", help="Month (YYYY-MM)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
show_usage()
|
||||
return 1
|
||||
|
||||
commands = {
|
||||
"create": cmd_create,
|
||||
"init-context": cmd_init_context,
|
||||
"add-context": cmd_add_context,
|
||||
"validate": cmd_validate,
|
||||
"list-context": cmd_list_context,
|
||||
"start": cmd_start,
|
||||
"finish": cmd_finish,
|
||||
"set-branch": cmd_set_branch,
|
||||
"set-base-branch": cmd_set_base_branch,
|
||||
"set-scope": cmd_set_scope,
|
||||
"create-pr": cmd_create_pr,
|
||||
"archive": cmd_archive,
|
||||
"add-subtask": cmd_add_subtask,
|
||||
"remove-subtask": cmd_remove_subtask,
|
||||
"list": cmd_list,
|
||||
"list-archive": cmd_list_archive,
|
||||
}
|
||||
|
||||
if args.command in commands:
|
||||
return commands[args.command](args)
|
||||
else:
|
||||
show_usage()
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
51
.trellis/spec/backend/database-guidelines.md
Normal file
51
.trellis/spec/backend/database-guidelines.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Database Guidelines
|
||||
|
||||
> Database patterns and conventions for this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's database conventions here.
|
||||
|
||||
Questions to answer:
|
||||
- What ORM/query library do you use?
|
||||
- How are migrations managed?
|
||||
- What are the naming conventions for tables/columns?
|
||||
- How do you handle transactions?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Query Patterns
|
||||
|
||||
<!-- How should queries be written? Batch operations? -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Migrations
|
||||
|
||||
<!-- How to create and run migrations -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
<!-- Table names, column names, index names -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
<!-- Database-related mistakes your team has made -->
|
||||
|
||||
(To be filled by the team)
|
||||
54
.trellis/spec/backend/directory-structure.md
Normal file
54
.trellis/spec/backend/directory-structure.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Directory Structure
|
||||
|
||||
> How backend code is organized in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's backend directory structure here.
|
||||
|
||||
Questions to answer:
|
||||
- How are modules/packages organized?
|
||||
- Where does business logic live?
|
||||
- Where are API endpoints defined?
|
||||
- How are utilities and helpers organized?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
<!-- Replace with your actual structure -->
|
||||
src/
|
||||
├── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Organization
|
||||
|
||||
<!-- How should new features/modules be organized? -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
<!-- File and folder naming rules -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
<!-- Link to well-organized modules as examples -->
|
||||
|
||||
(To be filled by the team)
|
||||
51
.trellis/spec/backend/error-handling.md
Normal file
51
.trellis/spec/backend/error-handling.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Error Handling
|
||||
|
||||
> How errors are handled in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's error handling conventions here.
|
||||
|
||||
Questions to answer:
|
||||
- What error types do you define?
|
||||
- How are errors propagated?
|
||||
- How are errors logged?
|
||||
- How are errors returned to clients?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Error Types
|
||||
|
||||
<!-- Custom error classes/types -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
<!-- Try-catch patterns, error propagation -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## API Error Responses
|
||||
|
||||
<!-- Standard error response format -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
<!-- Error handling mistakes your team has made -->
|
||||
|
||||
(To be filled by the team)
|
||||
38
.trellis/spec/backend/index.md
Normal file
38
.trellis/spec/backend/index.md
Normal file
@ -0,0 +1,38 @@
|
||||
# Backend Development Guidelines
|
||||
|
||||
> Best practices for backend development in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains guidelines for backend development. Fill in each file with your project's specific conventions.
|
||||
|
||||
---
|
||||
|
||||
## Guidelines Index
|
||||
|
||||
| Guide | Description | Status |
|
||||
|-------|-------------|--------|
|
||||
| [Directory Structure](./directory-structure.md) | Module organization and file layout | To fill |
|
||||
| [Database Guidelines](./database-guidelines.md) | ORM patterns, queries, migrations | To fill |
|
||||
| [Error Handling](./error-handling.md) | Error types, handling strategies | To fill |
|
||||
| [Quality Guidelines](./quality-guidelines.md) | Code standards, forbidden patterns | To fill |
|
||||
| [Logging Guidelines](./logging-guidelines.md) | Structured logging, log levels | To fill |
|
||||
|
||||
---
|
||||
|
||||
## How to Fill These Guidelines
|
||||
|
||||
For each guideline file:
|
||||
|
||||
1. Document your project's **actual conventions** (not ideals)
|
||||
2. Include **code examples** from your codebase
|
||||
3. List **forbidden patterns** and why
|
||||
4. Add **common mistakes** your team has made
|
||||
|
||||
The goal is to help AI assistants and new team members understand how YOUR project works.
|
||||
|
||||
---
|
||||
|
||||
**Language**: All documentation should be written in **English**.
|
||||
51
.trellis/spec/backend/logging-guidelines.md
Normal file
51
.trellis/spec/backend/logging-guidelines.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Logging Guidelines
|
||||
|
||||
> How logging is done in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's logging conventions here.
|
||||
|
||||
Questions to answer:
|
||||
- What logging library do you use?
|
||||
- What are the log levels and when to use each?
|
||||
- What should be logged?
|
||||
- What should NOT be logged (PII, secrets)?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Log Levels
|
||||
|
||||
<!-- When to use each level: debug, info, warn, error -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Structured Logging
|
||||
|
||||
<!-- Log format, required fields -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## What to Log
|
||||
|
||||
<!-- Important events to log -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Log
|
||||
|
||||
<!-- Sensitive data, PII, secrets -->
|
||||
|
||||
(To be filled by the team)
|
||||
51
.trellis/spec/backend/quality-guidelines.md
Normal file
51
.trellis/spec/backend/quality-guidelines.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Quality Guidelines
|
||||
|
||||
> Code quality standards for backend development.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's quality standards here.
|
||||
|
||||
Questions to answer:
|
||||
- What patterns are forbidden?
|
||||
- What linting rules do you enforce?
|
||||
- What are your testing requirements?
|
||||
- What code review standards apply?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Forbidden Patterns
|
||||
|
||||
<!-- Patterns that should never be used and why -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Required Patterns
|
||||
|
||||
<!-- Patterns that must always be used -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
<!-- What level of testing is expected -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
<!-- What reviewers should check -->
|
||||
|
||||
(To be filled by the team)
|
||||
59
.trellis/spec/frontend/component-guidelines.md
Normal file
59
.trellis/spec/frontend/component-guidelines.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Component Guidelines
|
||||
|
||||
> How components are built in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's component conventions here.
|
||||
|
||||
Questions to answer:
|
||||
- What component patterns do you use?
|
||||
- How are props defined?
|
||||
- How do you handle composition?
|
||||
- What accessibility standards apply?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Component Structure
|
||||
|
||||
<!-- Standard structure of a component file -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Props Conventions
|
||||
|
||||
<!-- How props should be defined and typed -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Styling Patterns
|
||||
|
||||
<!-- How styles are applied (CSS modules, styled-components, Tailwind, etc.) -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
<!-- A11y requirements and patterns -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
<!-- Component-related mistakes your team has made -->
|
||||
|
||||
(To be filled by the team)
|
||||
54
.trellis/spec/frontend/directory-structure.md
Normal file
54
.trellis/spec/frontend/directory-structure.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Directory Structure
|
||||
|
||||
> How frontend code is organized in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's frontend directory structure here.
|
||||
|
||||
Questions to answer:
|
||||
- Where do components live?
|
||||
- How are features/modules organized?
|
||||
- Where are shared utilities?
|
||||
- How are assets organized?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
<!-- Replace with your actual structure -->
|
||||
src/
|
||||
├── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Organization
|
||||
|
||||
<!-- How should new features be organized? -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
<!-- File and folder naming rules -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
<!-- Link to well-organized modules as examples -->
|
||||
|
||||
(To be filled by the team)
|
||||
51
.trellis/spec/frontend/hook-guidelines.md
Normal file
51
.trellis/spec/frontend/hook-guidelines.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Hook Guidelines
|
||||
|
||||
> How hooks are used in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's hook conventions here.
|
||||
|
||||
Questions to answer:
|
||||
- What custom hooks do you have?
|
||||
- How do you handle data fetching?
|
||||
- What are the naming conventions?
|
||||
- How do you share stateful logic?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Custom Hook Patterns
|
||||
|
||||
<!-- How to create and structure custom hooks -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Data Fetching
|
||||
|
||||
<!-- How data fetching is handled (React Query, SWR, etc.) -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
<!-- Hook naming rules (use*, etc.) -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
<!-- Hook-related mistakes your team has made -->
|
||||
|
||||
(To be filled by the team)
|
||||
39
.trellis/spec/frontend/index.md
Normal file
39
.trellis/spec/frontend/index.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Frontend Development Guidelines
|
||||
|
||||
> Best practices for frontend development in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains guidelines for frontend development. Fill in each file with your project's specific conventions.
|
||||
|
||||
---
|
||||
|
||||
## Guidelines Index
|
||||
|
||||
| Guide | Description | Status |
|
||||
|-------|-------------|--------|
|
||||
| [Directory Structure](./directory-structure.md) | Module organization and file layout | To fill |
|
||||
| [Component Guidelines](./component-guidelines.md) | Component patterns, props, composition | To fill |
|
||||
| [Hook Guidelines](./hook-guidelines.md) | Custom hooks, data fetching patterns | To fill |
|
||||
| [State Management](./state-management.md) | Local state, global state, server state | To fill |
|
||||
| [Quality Guidelines](./quality-guidelines.md) | Code standards, forbidden patterns | To fill |
|
||||
| [Type Safety](./type-safety.md) | Type patterns, validation | To fill |
|
||||
|
||||
---
|
||||
|
||||
## How to Fill These Guidelines
|
||||
|
||||
For each guideline file:
|
||||
|
||||
1. Document your project's **actual conventions** (not ideals)
|
||||
2. Include **code examples** from your codebase
|
||||
3. List **forbidden patterns** and why
|
||||
4. Add **common mistakes** your team has made
|
||||
|
||||
The goal is to help AI assistants and new team members understand how YOUR project works.
|
||||
|
||||
---
|
||||
|
||||
**Language**: All documentation should be written in **English**.
|
||||
51
.trellis/spec/frontend/quality-guidelines.md
Normal file
51
.trellis/spec/frontend/quality-guidelines.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Quality Guidelines
|
||||
|
||||
> Code quality standards for frontend development.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's quality standards here.
|
||||
|
||||
Questions to answer:
|
||||
- What patterns are forbidden?
|
||||
- What linting rules do you enforce?
|
||||
- What are your testing requirements?
|
||||
- What code review standards apply?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Forbidden Patterns
|
||||
|
||||
<!-- Patterns that should never be used and why -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Required Patterns
|
||||
|
||||
<!-- Patterns that must always be used -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
<!-- What level of testing is expected -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
<!-- What reviewers should check -->
|
||||
|
||||
(To be filled by the team)
|
||||
51
.trellis/spec/frontend/state-management.md
Normal file
51
.trellis/spec/frontend/state-management.md
Normal file
@ -0,0 +1,51 @@
|
||||
# State Management
|
||||
|
||||
> How state is managed in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's state management conventions here.
|
||||
|
||||
Questions to answer:
|
||||
- What state management solution do you use?
|
||||
- How is local vs global state decided?
|
||||
- How do you handle server state?
|
||||
- What are the patterns for derived state?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## State Categories
|
||||
|
||||
<!-- Local state, global state, server state, URL state -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## When to Use Global State
|
||||
|
||||
<!-- Criteria for promoting state to global -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Server State
|
||||
|
||||
<!-- How server data is cached and synchronized -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
<!-- State management mistakes your team has made -->
|
||||
|
||||
(To be filled by the team)
|
||||
51
.trellis/spec/frontend/type-safety.md
Normal file
51
.trellis/spec/frontend/type-safety.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Type Safety
|
||||
|
||||
> Type safety patterns in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<!--
|
||||
Document your project's type safety conventions here.
|
||||
|
||||
Questions to answer:
|
||||
- What type system do you use?
|
||||
- How are types organized?
|
||||
- What validation library do you use?
|
||||
- How do you handle type inference?
|
||||
-->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Type Organization
|
||||
|
||||
<!-- Where types are defined, shared types vs local types -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
<!-- Runtime validation patterns (Zod, Yup, io-ts, etc.) -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
<!-- Type utilities, generics, type guards -->
|
||||
|
||||
(To be filled by the team)
|
||||
|
||||
---
|
||||
|
||||
## Forbidden Patterns
|
||||
|
||||
<!-- any, type assertions, etc. -->
|
||||
|
||||
(To be filled by the team)
|
||||
105
.trellis/spec/guides/code-reuse-thinking-guide.md
Normal file
105
.trellis/spec/guides/code-reuse-thinking-guide.md
Normal file
@ -0,0 +1,105 @@
|
||||
# Code Reuse Thinking Guide
|
||||
|
||||
> **Purpose**: Stop and think before creating new code - does it already exist?
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
**Duplicated code is the #1 source of inconsistency bugs.**
|
||||
|
||||
When you copy-paste or rewrite existing logic:
|
||||
- Bug fixes don't propagate
|
||||
- Behavior diverges over time
|
||||
- Codebase becomes harder to understand
|
||||
|
||||
---
|
||||
|
||||
## Before Writing New Code
|
||||
|
||||
### Step 1: Search First
|
||||
|
||||
```bash
|
||||
# Search for similar function names
|
||||
grep -r "functionName" .
|
||||
|
||||
# Search for similar logic
|
||||
grep -r "keyword" .
|
||||
```
|
||||
|
||||
### Step 2: Ask These Questions
|
||||
|
||||
| Question | If Yes... |
|
||||
|----------|-----------|
|
||||
| Does a similar function exist? | Use or extend it |
|
||||
| Is this pattern used elsewhere? | Follow the existing pattern |
|
||||
| Could this be a shared utility? | Create it in the right place |
|
||||
| Am I copying code from another file? | **STOP** - extract to shared |
|
||||
|
||||
---
|
||||
|
||||
## Common Duplication Patterns
|
||||
|
||||
### Pattern 1: Copy-Paste Functions
|
||||
|
||||
**Bad**: Copying a validation function to another file
|
||||
|
||||
**Good**: Extract to shared utilities, import where needed
|
||||
|
||||
### Pattern 2: Similar Components
|
||||
|
||||
**Bad**: Creating a new component that's 80% similar to existing
|
||||
|
||||
**Good**: Extend existing component with props/variants
|
||||
|
||||
### Pattern 3: Repeated Constants
|
||||
|
||||
**Bad**: Defining the same constant in multiple files
|
||||
|
||||
**Good**: Single source of truth, import everywhere
|
||||
|
||||
---
|
||||
|
||||
## When to Abstract
|
||||
|
||||
**Abstract when**:
|
||||
- Same code appears 3+ times
|
||||
- Logic is complex enough to have bugs
|
||||
- Multiple people might need this
|
||||
|
||||
**Don't abstract when**:
|
||||
- Only used once
|
||||
- Trivial one-liner
|
||||
- Abstraction would be more complex than duplication
|
||||
|
||||
---
|
||||
|
||||
## After Batch Modifications
|
||||
|
||||
When you've made similar changes to multiple files:
|
||||
|
||||
1. **Review**: Did you catch all instances?
|
||||
2. **Search**: Run grep to find any missed
|
||||
3. **Consider**: Should this be abstracted?
|
||||
|
||||
---
|
||||
|
||||
## Gotcha: Asymmetric Mechanisms Producing Same Output
|
||||
|
||||
**Problem**: When two different mechanisms must produce the same file set (e.g., recursive directory copy for init vs. manual `files.set()` for update), structural changes (renaming, moving, adding subdirectories) only propagate through the automatic mechanism. The manual one silently drifts.
|
||||
|
||||
**Symptom**: Init works perfectly, but update creates files at wrong paths or misses files entirely.
|
||||
|
||||
**Prevention checklist**:
|
||||
- [ ] When migrating directory structures, search for ALL code paths that reference the old structure
|
||||
- [ ] If one path is auto-derived (glob/copy) and another is manually listed, the manual one needs updating
|
||||
- [ ] Add a regression test that compares outputs from both mechanisms
|
||||
|
||||
---
|
||||
|
||||
## Checklist Before Commit
|
||||
|
||||
- [ ] Searched for existing similar code
|
||||
- [ ] No copy-pasted logic that should be shared
|
||||
- [ ] Constants defined in one place
|
||||
- [ ] Similar patterns follow same structure
|
||||
94
.trellis/spec/guides/cross-layer-thinking-guide.md
Normal file
94
.trellis/spec/guides/cross-layer-thinking-guide.md
Normal file
@ -0,0 +1,94 @@
|
||||
# Cross-Layer Thinking Guide
|
||||
|
||||
> **Purpose**: Think through data flow across layers before implementing.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
**Most bugs happen at layer boundaries**, not within layers.
|
||||
|
||||
Common cross-layer bugs:
|
||||
- API returns format A, frontend expects format B
|
||||
- Database stores X, service transforms to Y, but loses data
|
||||
- Multiple layers implement the same logic differently
|
||||
|
||||
---
|
||||
|
||||
## Before Implementing Cross-Layer Features
|
||||
|
||||
### Step 1: Map the Data Flow
|
||||
|
||||
Draw out how data moves:
|
||||
|
||||
```
|
||||
Source → Transform → Store → Retrieve → Transform → Display
|
||||
```
|
||||
|
||||
For each arrow, ask:
|
||||
- What format is the data in?
|
||||
- What could go wrong?
|
||||
- Who is responsible for validation?
|
||||
|
||||
### Step 2: Identify Boundaries
|
||||
|
||||
| Boundary | Common Issues |
|
||||
|----------|---------------|
|
||||
| API ↔ Service | Type mismatches, missing fields |
|
||||
| Service ↔ Database | Format conversions, null handling |
|
||||
| Backend ↔ Frontend | Serialization, date formats |
|
||||
| Component ↔ Component | Props shape changes |
|
||||
|
||||
### Step 3: Define Contracts
|
||||
|
||||
For each boundary:
|
||||
- What is the exact input format?
|
||||
- What is the exact output format?
|
||||
- What errors can occur?
|
||||
|
||||
---
|
||||
|
||||
## Common Cross-Layer Mistakes
|
||||
|
||||
### Mistake 1: Implicit Format Assumptions
|
||||
|
||||
**Bad**: Assuming date format without checking
|
||||
|
||||
**Good**: Explicit format conversion at boundaries
|
||||
|
||||
### Mistake 2: Scattered Validation
|
||||
|
||||
**Bad**: Validating the same thing in multiple layers
|
||||
|
||||
**Good**: Validate once at the entry point
|
||||
|
||||
### Mistake 3: Leaky Abstractions
|
||||
|
||||
**Bad**: Component knows about database schema
|
||||
|
||||
**Good**: Each layer only knows its neighbors
|
||||
|
||||
---
|
||||
|
||||
## Checklist for Cross-Layer Features
|
||||
|
||||
Before implementation:
|
||||
- [ ] Mapped the complete data flow
|
||||
- [ ] Identified all layer boundaries
|
||||
- [ ] Defined format at each boundary
|
||||
- [ ] Decided where validation happens
|
||||
|
||||
After implementation:
|
||||
- [ ] Tested with edge cases (null, empty, invalid)
|
||||
- [ ] Verified error handling at each boundary
|
||||
- [ ] Checked data survives round-trip
|
||||
|
||||
---
|
||||
|
||||
## When to Create Flow Documentation
|
||||
|
||||
Create detailed flow docs when:
|
||||
- Feature spans 3+ layers
|
||||
- Multiple teams are involved
|
||||
- Data format is complex
|
||||
- Feature has caused bugs before
|
||||
79
.trellis/spec/guides/index.md
Normal file
79
.trellis/spec/guides/index.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Thinking Guides
|
||||
|
||||
> **Purpose**: Expand your thinking to catch things you might not have considered.
|
||||
|
||||
---
|
||||
|
||||
## Why Thinking Guides?
|
||||
|
||||
**Most bugs and tech debt come from "didn't think of that"**, not from lack of skill:
|
||||
|
||||
- Didn't think about what happens at layer boundaries → cross-layer bugs
|
||||
- Didn't think about code patterns repeating → duplicated code everywhere
|
||||
- Didn't think about edge cases → runtime errors
|
||||
- Didn't think about future maintainers → unreadable code
|
||||
|
||||
These guides help you **ask the right questions before coding**.
|
||||
|
||||
---
|
||||
|
||||
## Available Guides
|
||||
|
||||
| Guide | Purpose | When to Use |
|
||||
|-------|---------|-------------|
|
||||
| [Code Reuse Thinking Guide](./code-reuse-thinking-guide.md) | Identify patterns and reduce duplication | When you notice repeated patterns |
|
||||
| [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md) | Think through data flow across layers | Features spanning multiple layers |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Thinking Triggers
|
||||
|
||||
### When to Think About Cross-Layer Issues
|
||||
|
||||
- [ ] Feature touches 3+ layers (API, Service, Component, Database)
|
||||
- [ ] Data format changes between layers
|
||||
- [ ] Multiple consumers need the same data
|
||||
- [ ] You're not sure where to put some logic
|
||||
|
||||
→ Read [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md)
|
||||
|
||||
### When to Think About Code Reuse
|
||||
|
||||
- [ ] You're writing similar code to something that exists
|
||||
- [ ] You see the same pattern repeated 3+ times
|
||||
- [ ] You're adding a new field to multiple places
|
||||
- [ ] **You're modifying any constant or config**
|
||||
- [ ] **You're creating a new utility/helper function** ← Search first!
|
||||
|
||||
→ Read [Code Reuse Thinking Guide](./code-reuse-thinking-guide.md)
|
||||
|
||||
---
|
||||
|
||||
## Pre-Modification Rule (CRITICAL)
|
||||
|
||||
> **Before changing ANY value, ALWAYS search first!**
|
||||
|
||||
```bash
|
||||
# Search for the value you're about to change
|
||||
grep -r "value_to_change" .
|
||||
```
|
||||
|
||||
This single habit prevents most "forgot to update X" bugs.
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Directory
|
||||
|
||||
1. **Before coding**: Skim the relevant thinking guide
|
||||
2. **During coding**: If something feels repetitive or complex, check the guides
|
||||
3. **After bugs**: Add new insights to the relevant guide (learn from mistakes)
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Found a new "didn't think of that" moment? Add it to the relevant guide.
|
||||
|
||||
---
|
||||
|
||||
**Core Principle**: 30 minutes of thinking saves 3 hours of debugging.
|
||||
113
.trellis/tasks/00-bootstrap-guidelines/prd.md
Normal file
113
.trellis/tasks/00-bootstrap-guidelines/prd.md
Normal file
@ -0,0 +1,113 @@
|
||||
# Bootstrap: Fill Project Development Guidelines
|
||||
|
||||
## Purpose
|
||||
|
||||
Welcome to Trellis! This is your first task.
|
||||
|
||||
AI agents use `.trellis/spec/` to understand YOUR project's coding conventions.
|
||||
**Starting from scratch = AI writes generic code that doesn't match your project style.**
|
||||
|
||||
Filling these guidelines is a one-time setup that pays off for every future AI session.
|
||||
|
||||
---
|
||||
|
||||
## Your Task
|
||||
|
||||
Fill in the guideline files based on your **existing codebase**.
|
||||
|
||||
|
||||
### Backend Guidelines
|
||||
|
||||
| File | What to Document |
|
||||
|------|------------------|
|
||||
| `.trellis/spec/backend/directory-structure.md` | Where different file types go (routes, services, utils) |
|
||||
| `.trellis/spec/backend/database-guidelines.md` | ORM, migrations, query patterns, naming conventions |
|
||||
| `.trellis/spec/backend/error-handling.md` | How errors are caught, logged, and returned |
|
||||
| `.trellis/spec/backend/logging-guidelines.md` | Log levels, format, what to log |
|
||||
| `.trellis/spec/backend/quality-guidelines.md` | Code review standards, testing requirements |
|
||||
|
||||
|
||||
### Frontend Guidelines
|
||||
|
||||
| File | What to Document |
|
||||
|------|------------------|
|
||||
| `.trellis/spec/frontend/directory-structure.md` | Component/page/hook organization |
|
||||
| `.trellis/spec/frontend/component-guidelines.md` | Component patterns, props conventions |
|
||||
| `.trellis/spec/frontend/hook-guidelines.md` | Custom hook naming, patterns |
|
||||
| `.trellis/spec/frontend/state-management.md` | State library, patterns, what goes where |
|
||||
| `.trellis/spec/frontend/type-safety.md` | TypeScript conventions, type organization |
|
||||
| `.trellis/spec/frontend/quality-guidelines.md` | Linting, testing, accessibility |
|
||||
|
||||
|
||||
### Thinking Guides (Optional)
|
||||
|
||||
The `.trellis/spec/guides/` directory contains thinking guides that are already
|
||||
filled with general best practices. You can customize them for your project if needed.
|
||||
|
||||
---
|
||||
|
||||
## How to Fill Guidelines
|
||||
|
||||
### Step 0: Import from Existing Specs (Recommended)
|
||||
|
||||
Many projects already have coding conventions documented. **Check these first** before writing from scratch:
|
||||
|
||||
| File / Directory | Tool |
|
||||
|------|------|
|
||||
| `CLAUDE.md` / `CLAUDE.local.md` | Claude Code |
|
||||
| `AGENTS.md` | Codex / Claude Code / agent-compatible tools |
|
||||
| `.cursorrules` | Cursor |
|
||||
| `.cursor/rules/*.mdc` | Cursor (rules directory) |
|
||||
| `.windsurfrules` | Windsurf |
|
||||
| `.clinerules` | Cline |
|
||||
| `.roomodes` | Roo Code |
|
||||
| `.github/copilot-instructions.md` | GitHub Copilot |
|
||||
| `.vscode/settings.json` → `github.copilot.chat.codeGeneration.instructions` | VS Code Copilot |
|
||||
| `CONVENTIONS.md` / `.aider.conf.yml` | aider |
|
||||
| `CONTRIBUTING.md` | General project conventions |
|
||||
| `.editorconfig` | Editor formatting rules |
|
||||
|
||||
If any of these exist, read them first and extract the relevant coding conventions into the corresponding `.trellis/spec/` files. This saves significant effort compared to writing everything from scratch.
|
||||
|
||||
### Step 1: Analyze the Codebase
|
||||
|
||||
Ask AI to help discover patterns from actual code:
|
||||
|
||||
- "Read all existing config files (CLAUDE.md, .cursorrules, etc.) and extract coding conventions into .trellis/spec/"
|
||||
- "Analyze my codebase and document the patterns you see"
|
||||
- "Find error handling / component / API patterns and document them"
|
||||
|
||||
### Step 2: Document Reality, Not Ideals
|
||||
|
||||
Write what your codebase **actually does**, not what you wish it did.
|
||||
AI needs to match existing patterns, not introduce new ones.
|
||||
|
||||
- **Look at existing code** - Find 2-3 examples of each pattern
|
||||
- **Include file paths** - Reference real files as examples
|
||||
- **List anti-patterns** - What does your team avoid?
|
||||
|
||||
---
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
- [ ] Guidelines filled for your project type
|
||||
- [ ] At least 2-3 real code examples in each guideline
|
||||
- [ ] Anti-patterns documented
|
||||
|
||||
When done:
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py finish
|
||||
python3 ./.trellis/scripts/task.py archive 00-bootstrap-guidelines
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why This Matters
|
||||
|
||||
After completing this task:
|
||||
|
||||
1. AI will write code that matches your project style
|
||||
2. Relevant `/trellis:before-*-dev` commands will inject real context
|
||||
3. `/trellis:check-*` commands will validate against your actual standards
|
||||
4. Future developers (human or AI) will onboard faster
|
||||
35
.trellis/tasks/00-bootstrap-guidelines/task.json
Normal file
35
.trellis/tasks/00-bootstrap-guidelines/task.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "00-bootstrap-guidelines",
|
||||
"name": "Bootstrap Guidelines",
|
||||
"description": "Fill in project development guidelines for AI agents",
|
||||
"status": "in_progress",
|
||||
"dev_type": "docs",
|
||||
"priority": "P1",
|
||||
"creator": "Apale",
|
||||
"assignee": "Apale",
|
||||
"createdAt": "2026-04-16",
|
||||
"completedAt": null,
|
||||
"commit": null,
|
||||
"subtasks": [
|
||||
{
|
||||
"name": "Fill backend guidelines",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"name": "Fill frontend guidelines",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"name": "Add code examples",
|
||||
"status": "pending"
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [
|
||||
".trellis/spec/backend/",
|
||||
".trellis/spec/frontend/"
|
||||
],
|
||||
"notes": "First-time setup task created by trellis init (fullstack project)",
|
||||
"meta": {}
|
||||
}
|
||||
416
.trellis/workflow.md
Normal file
416
.trellis/workflow.md
Normal file
@ -0,0 +1,416 @@
|
||||
# Development Workflow
|
||||
|
||||
> Based on [Effective Harnesses for Long-Running Agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Quick Start (Do This First)](#quick-start-do-this-first)
|
||||
2. [Workflow Overview](#workflow-overview)
|
||||
3. [Session Start Process](#session-start-process)
|
||||
4. [Development Process](#development-process)
|
||||
5. [Session End](#session-end)
|
||||
6. [File Descriptions](#file-descriptions)
|
||||
7. [Best Practices](#best-practices)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Do This First)
|
||||
|
||||
### Step 0: Initialize Developer Identity (First Time Only)
|
||||
|
||||
> **Multi-developer support**: Each developer/Agent needs to initialize their identity first
|
||||
|
||||
```bash
|
||||
# Check if already initialized
|
||||
python3 ./.trellis/scripts/get_developer.py
|
||||
|
||||
# If not initialized, run:
|
||||
python3 ./.trellis/scripts/init_developer.py <your-name>
|
||||
# Example: python3 ./.trellis/scripts/init_developer.py cursor-agent
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `.trellis/.developer` - Your identity file (gitignored, not committed)
|
||||
- `.trellis/workspace/<your-name>/` - Your personal workspace directory
|
||||
|
||||
**Naming suggestions**:
|
||||
- Human developers: Use your name, e.g., `john-doe`
|
||||
- Cursor AI: `cursor-agent` or `cursor-<task>`
|
||||
- Claude Code: `claude-agent` or `claude-<task>`
|
||||
- iFlow cli: `iflow-agent` or `iflow-<task>`
|
||||
|
||||
### Step 1: Understand Current Context
|
||||
|
||||
```bash
|
||||
# Get full context in one command
|
||||
python3 ./.trellis/scripts/get_context.py
|
||||
|
||||
# Or check manually:
|
||||
python3 ./.trellis/scripts/get_developer.py # Your identity
|
||||
python3 ./.trellis/scripts/task.py list # Active tasks
|
||||
git status && git log --oneline -10 # Git state
|
||||
```
|
||||
|
||||
### Step 2: Read Project Guidelines [MANDATORY]
|
||||
|
||||
**CRITICAL**: Read guidelines before writing any code:
|
||||
|
||||
```bash
|
||||
# Discover available packages and spec layers
|
||||
python3 ./.trellis/scripts/get_context.py --mode packages
|
||||
|
||||
# Read the spec index for each relevant module
|
||||
cat .trellis/spec/<package>/<layer>/index.md
|
||||
|
||||
# Always read shared guides
|
||||
cat .trellis/spec/guides/index.md
|
||||
```
|
||||
|
||||
**Why this matters?**
|
||||
- Understand which spec layers apply to your task
|
||||
- Know coding standards for the packages you'll modify
|
||||
- Learn the overall code quality requirements
|
||||
|
||||
### Step 3: Before Coding - Read Specific Guidelines (Required)
|
||||
|
||||
Based on your task, read the **detailed** guideline files listed in each spec index's **Pre-Development Checklist**:
|
||||
|
||||
```bash
|
||||
# The index points to specific files — read those, not just the index
|
||||
cat .trellis/spec/<package>/<layer>/error-handling.md
|
||||
cat .trellis/spec/<package>/<layer>/conventions.md
|
||||
# etc. — based on what the Pre-Development Checklist lists
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Read Before Write** - Understand context before starting
|
||||
2. **Follow Standards** - [!] **MUST read `.trellis/spec/` guidelines before coding**
|
||||
3. **Incremental Development** - Complete one task at a time
|
||||
4. **Record Promptly** - Update tracking files immediately after completion
|
||||
5. **Document Limits** - [!] **Max 2000 lines per journal document**
|
||||
|
||||
### File System
|
||||
|
||||
```
|
||||
.trellis/
|
||||
|-- .developer # Developer identity (gitignored)
|
||||
|-- scripts/
|
||||
| |-- __init__.py # Python package init
|
||||
| |-- common/ # Shared utilities (Python)
|
||||
| | |-- __init__.py
|
||||
| | |-- paths.py # Path utilities
|
||||
| | |-- developer.py # Developer management
|
||||
| | +-- git_context.py # Git context implementation
|
||||
| |-- multi_agent/ # Multi-agent pipeline scripts
|
||||
| | |-- __init__.py
|
||||
| | |-- start.py # Start worktree agent
|
||||
| | |-- status.py # Monitor agent status
|
||||
| | |-- create_pr.py # Create PR
|
||||
| | +-- cleanup.py # Cleanup worktree
|
||||
| |-- init_developer.py # Initialize developer identity
|
||||
| |-- get_developer.py # Get current developer name
|
||||
| |-- task.py # Manage tasks
|
||||
| |-- get_context.py # Get session context
|
||||
| +-- add_session.py # One-click session recording
|
||||
|-- workspace/ # Developer workspaces
|
||||
| |-- index.md # Workspace index + Session template
|
||||
| +-- {developer}/ # Per-developer directories
|
||||
| |-- index.md # Personal index (with @@@auto markers)
|
||||
| +-- journal-N.md # Journal files (sequential numbering)
|
||||
|-- tasks/ # Task tracking
|
||||
| +-- {MM}-{DD}-{name}/
|
||||
| +-- task.json
|
||||
|-- spec/ # [!] MUST READ before coding
|
||||
| |-- frontend/ # Frontend guidelines (if applicable)
|
||||
| | |-- index.md # Start here - guidelines index
|
||||
| | +-- *.md # Topic-specific docs
|
||||
| |-- backend/ # Backend guidelines (if applicable)
|
||||
| | |-- index.md # Start here - guidelines index
|
||||
| | +-- *.md # Topic-specific docs
|
||||
| +-- guides/ # Thinking guides
|
||||
| |-- index.md # Guides index
|
||||
| |-- cross-layer-thinking-guide.md # Pre-implementation checklist
|
||||
| +-- *.md # Other guides
|
||||
+-- workflow.md # This document
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Start Process
|
||||
|
||||
### Step 1: Get Session Context
|
||||
|
||||
Use the unified context script:
|
||||
|
||||
```bash
|
||||
# Get all context in one command
|
||||
python3 ./.trellis/scripts/get_context.py
|
||||
|
||||
# Or get JSON format
|
||||
python3 ./.trellis/scripts/get_context.py --json
|
||||
```
|
||||
|
||||
### Step 2: Read Development Guidelines [!] REQUIRED
|
||||
|
||||
**[!] CRITICAL: MUST read guidelines before writing any code**
|
||||
|
||||
Based on what you'll develop, read the corresponding guidelines:
|
||||
|
||||
```bash
|
||||
# Discover available packages and spec layers
|
||||
python3 ./.trellis/scripts/get_context.py --mode packages
|
||||
|
||||
# Read spec indexes for relevant modules
|
||||
cat .trellis/spec/<package>/<layer>/index.md
|
||||
|
||||
# For cross-layer features
|
||||
cat .trellis/spec/guides/cross-layer-thinking-guide.md
|
||||
```
|
||||
|
||||
### Step 3: Select Task to Develop
|
||||
|
||||
Use the task management script:
|
||||
|
||||
```bash
|
||||
# List active tasks
|
||||
python3 ./.trellis/scripts/task.py list
|
||||
|
||||
# Create new task (creates directory with task.json)
|
||||
python3 ./.trellis/scripts/task.py create "<title>" --slug <task-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Process
|
||||
|
||||
### Task Development Flow
|
||||
|
||||
```
|
||||
1. Create or select task
|
||||
--> python3 ./.trellis/scripts/task.py create "<title>" --slug <name> or list
|
||||
|
||||
2. Start task (mark as current)
|
||||
--> python3 ./.trellis/scripts/task.py start <name>
|
||||
--> Writes .trellis/.current-task; future sessions see it in <current-state>
|
||||
|
||||
3. Write code according to guidelines
|
||||
--> Read .trellis/spec/ docs relevant to your task
|
||||
--> For cross-layer: read .trellis/spec/guides/
|
||||
|
||||
4. Self-test
|
||||
--> Run project's lint/test commands (see spec docs)
|
||||
--> Manual feature testing
|
||||
|
||||
5. Commit code
|
||||
--> git add <files>
|
||||
--> git commit -m "type(scope): description"
|
||||
Format: feat/fix/docs/refactor/test/chore
|
||||
|
||||
6. Record session (one command)
|
||||
--> python3 ./.trellis/scripts/add_session.py --title "Title" --commit "hash"
|
||||
|
||||
7. Finish task (clear current)
|
||||
--> python3 ./.trellis/scripts/task.py finish
|
||||
--> Only when the task is fully done; otherwise leave it set so the
|
||||
next session resumes where you left off
|
||||
```
|
||||
|
||||
### Code Quality Checklist
|
||||
|
||||
**Must pass before commit**:
|
||||
- [OK] Lint checks pass (project-specific command)
|
||||
- [OK] Type checks pass (if applicable)
|
||||
- [OK] Manual feature testing passes
|
||||
|
||||
**Project-specific checks**:
|
||||
- See `.trellis/spec/<package>/<layer>/quality-guidelines.md` for package-specific checks
|
||||
|
||||
---
|
||||
|
||||
## Session End
|
||||
|
||||
### One-Click Session Recording
|
||||
|
||||
After code is committed, use:
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/add_session.py \
|
||||
--title "Session Title" \
|
||||
--commit "abc1234" \
|
||||
--summary "Brief summary"
|
||||
```
|
||||
|
||||
This automatically:
|
||||
1. Detects current journal file
|
||||
2. Creates new file if 2000-line limit exceeded
|
||||
3. Appends session content
|
||||
4. Updates index.md (sessions count, history table)
|
||||
|
||||
### Pre-end Checklist
|
||||
|
||||
Use `/trellis:finish-work` command to run through:
|
||||
1. [OK] All code committed, commit message follows convention
|
||||
2. [OK] Session recorded via `add_session.py`
|
||||
3. [OK] No lint/test errors
|
||||
4. [OK] Working directory clean (or WIP noted)
|
||||
5. [OK] Spec docs updated if needed
|
||||
|
||||
---
|
||||
|
||||
## File Descriptions
|
||||
|
||||
### 1. workspace/ - Developer Workspaces
|
||||
|
||||
**Purpose**: Record each AI Agent session's work content
|
||||
|
||||
**Structure** (Multi-developer support):
|
||||
```
|
||||
workspace/
|
||||
|-- index.md # Main index (Active Developers table)
|
||||
+-- {developer}/ # Per-developer directory
|
||||
|-- index.md # Personal index (with @@@auto markers)
|
||||
+-- journal-N.md # Journal files (sequential: 1, 2, 3...)
|
||||
```
|
||||
|
||||
**When to update**:
|
||||
- [OK] End of each session
|
||||
- [OK] Complete important task
|
||||
- [OK] Fix important bug
|
||||
|
||||
### 2. spec/ - Development Guidelines
|
||||
|
||||
**Purpose**: Documented standards for consistent development
|
||||
|
||||
**Structure** (Multi-doc format):
|
||||
```
|
||||
spec/
|
||||
|-- frontend/ # Frontend docs (if applicable)
|
||||
| |-- index.md # Start here
|
||||
| +-- *.md # Topic-specific docs
|
||||
|-- backend/ # Backend docs (if applicable)
|
||||
| |-- index.md # Start here
|
||||
| +-- *.md # Topic-specific docs
|
||||
+-- guides/ # Thinking guides
|
||||
|-- index.md # Start here
|
||||
+-- *.md # Guide-specific docs
|
||||
```
|
||||
|
||||
**When to update**:
|
||||
- [OK] New pattern discovered
|
||||
- [OK] Bug fixed that reveals missing guidance
|
||||
- [OK] New convention established
|
||||
|
||||
### 3. Tasks - Task Tracking
|
||||
|
||||
Each task is a directory containing `task.json`:
|
||||
|
||||
```
|
||||
tasks/
|
||||
|-- 01-21-my-task/
|
||||
| +-- task.json
|
||||
+-- archive/
|
||||
+-- 2026-01/
|
||||
+-- 01-15-old-task/
|
||||
+-- task.json
|
||||
```
|
||||
|
||||
**Commands**:
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py create "<title>" [--slug <name>] # Create task directory
|
||||
python3 ./.trellis/scripts/task.py start <name> # Set as current task (writes .current-task, triggers after_start hooks)
|
||||
python3 ./.trellis/scripts/task.py finish # Clear current task (triggers after_finish hooks)
|
||||
python3 ./.trellis/scripts/task.py archive <name> # Archive to archive/{year-month}/
|
||||
python3 ./.trellis/scripts/task.py list # List active tasks
|
||||
python3 ./.trellis/scripts/task.py list-archive # List archived tasks
|
||||
```
|
||||
|
||||
**Current task mechanism**: `task.py start <name>` writes the selected task path to `.trellis/.current-task`. The SessionStart hook reads this file to inject `## CURRENT TASK` into every new session's context, so the AI immediately knows what you're working on without being told. Run `task.py finish` when you're done — subsequent sessions will show `(none)` until you start another task.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### [OK] DO - Should Do
|
||||
|
||||
1. **Before session start**:
|
||||
- Run `python3 ./.trellis/scripts/get_context.py` for full context
|
||||
- [!] **MUST read** relevant `.trellis/spec/` docs
|
||||
|
||||
2. **During development**:
|
||||
- [!] **Follow** `.trellis/spec/` guidelines
|
||||
- For cross-layer features, use `/trellis:check-cross-layer`
|
||||
- Develop only one task at a time
|
||||
- Run lint and tests frequently
|
||||
|
||||
3. **After development complete**:
|
||||
- Use `/trellis:finish-work` for completion checklist
|
||||
- After fix bug, use `/trellis:break-loop` for deep analysis
|
||||
- Human commits after testing passes
|
||||
- Use `add_session.py` to record progress
|
||||
|
||||
### [X] DON'T - Should Not Do
|
||||
|
||||
1. [!] **Don't** skip reading `.trellis/spec/` guidelines
|
||||
2. [!] **Don't** let journal single file exceed 2000 lines
|
||||
3. **Don't** develop multiple unrelated tasks simultaneously
|
||||
4. **Don't** commit code with lint/test errors
|
||||
5. **Don't** forget to update spec docs after learning something
|
||||
6. [!] **Don't** execute `git commit` - AI should not commit code
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Must-read Before Development
|
||||
|
||||
| Task Type | Must-read Document |
|
||||
|-----------|-------------------|
|
||||
| Frontend work | `frontend/index.md` → relevant docs |
|
||||
| Backend work | `backend/index.md` → relevant docs |
|
||||
| Cross-Layer Feature | `guides/cross-layer-thinking-guide.md` |
|
||||
|
||||
### Commit Convention
|
||||
|
||||
```bash
|
||||
git commit -m "type(scope): description"
|
||||
```
|
||||
|
||||
**Type**: feat, fix, docs, refactor, test, chore
|
||||
**Scope**: Module name (e.g., auth, api, ui)
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# Session management
|
||||
python3 ./.trellis/scripts/get_context.py # Get full context
|
||||
python3 ./.trellis/scripts/add_session.py # Record session
|
||||
|
||||
# Task management
|
||||
python3 ./.trellis/scripts/task.py list # List tasks
|
||||
python3 ./.trellis/scripts/task.py create "<title>" # Create task
|
||||
|
||||
# Slash commands
|
||||
/trellis:finish-work # Pre-commit checklist
|
||||
/trellis:break-loop # Post-debug analysis
|
||||
/trellis:check-cross-layer # Cross-layer verification
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Following this workflow ensures:
|
||||
- [OK] Continuity across multiple sessions
|
||||
- [OK] Consistent code quality
|
||||
- [OK] Trackable progress
|
||||
- [OK] Knowledge accumulation in spec docs
|
||||
- [OK] Transparent team collaboration
|
||||
|
||||
**Core Philosophy**: Read before write, follow standards, record promptly, capture learnings
|
||||
40
.trellis/workspace/Apale/index.md
Normal file
40
.trellis/workspace/Apale/index.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Workspace Index - Apale
|
||||
|
||||
> 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
|
||||
7
.trellis/workspace/Apale/journal-1.md
Normal file
7
.trellis/workspace/Apale/journal-1.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Journal - Apale (Part 1)
|
||||
|
||||
> AI development session journal
|
||||
> Started: 2026-04-16
|
||||
|
||||
---
|
||||
|
||||
125
.trellis/workspace/index.md
Normal file
125
.trellis/workspace/index.md
Normal file
@ -0,0 +1,125 @@
|
||||
# Workspace Index
|
||||
|
||||
> Records of all AI Agent work records across all developers
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This directory tracks records for all developers working with AI Agents on this project.
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
workspace/
|
||||
|-- index.md # This file - main index
|
||||
+-- {developer}/ # Per-developer directory
|
||||
|-- index.md # Personal index with session history
|
||||
|-- tasks/ # Task files
|
||||
| |-- *.json # Active tasks
|
||||
| +-- archive/ # Archived tasks by month
|
||||
+-- journal-N.md # Journal files (sequential: 1, 2, 3...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Active Developers
|
||||
|
||||
| Developer | Last Active | Sessions | Active File |
|
||||
|-----------|-------------|----------|-------------|
|
||||
| (none yet) | - | - | - |
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### For New Developers
|
||||
|
||||
Run the initialization script:
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/init_developer.py <your-name>
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Create your identity file (gitignored)
|
||||
2. Create your progress directory
|
||||
3. Create your personal index
|
||||
4. Create initial journal file
|
||||
|
||||
### For Returning Developers
|
||||
|
||||
1. Get your developer name:
|
||||
```bash
|
||||
python3 ./.trellis/scripts/get_developer.py
|
||||
```
|
||||
|
||||
2. Read your personal index:
|
||||
```bash
|
||||
cat .trellis/workspace/$(python3 ./.trellis/scripts/get_developer.py)/index.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Journal File Rules
|
||||
|
||||
- **Max 2000 lines** per journal file
|
||||
- When limit is reached, create `journal-{N+1}.md`
|
||||
- Update your personal `index.md` when creating new files
|
||||
|
||||
### Session Record Format
|
||||
|
||||
Each session should include:
|
||||
- Summary: One-line description
|
||||
- Branch: Which branch the work was done on
|
||||
- Main Changes: What was modified
|
||||
- Git Commits: Commit hashes and messages
|
||||
- Next Steps: What to do next
|
||||
|
||||
---
|
||||
|
||||
## Session Template
|
||||
|
||||
Use this template when recording sessions:
|
||||
|
||||
```markdown
|
||||
## Session {N}: {Title}
|
||||
|
||||
**Date**: YYYY-MM-DD
|
||||
**Task**: {task-name}
|
||||
**Branch**: `{branch-name}`
|
||||
|
||||
### Summary
|
||||
|
||||
{One-line summary}
|
||||
|
||||
### Main Changes
|
||||
|
||||
- {Change 1}
|
||||
- {Change 2}
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `abc1234` | {commit message} |
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] {Test result}
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed** / # **In Progress** / [P] **Blocked**
|
||||
|
||||
### Next Steps
|
||||
|
||||
- {Next step 1}
|
||||
- {Next step 2}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Language**: All documentation must be written in **English**.
|
||||
47
.trellis/worktree.yaml
Normal file
47
.trellis/worktree.yaml
Normal file
@ -0,0 +1,47 @@
|
||||
# Worktree Configuration for Multi-Agent Pipeline
|
||||
# Used for worktree initialization in multi-agent workflows
|
||||
#
|
||||
# All paths are relative to project root
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Paths
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Worktree storage directory (relative to project root)
|
||||
worktree_dir: ../trellis-worktrees
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Files to Copy
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Files to copy to each worktree (each worktree needs independent copy)
|
||||
# These files contain sensitive info or need worktree-independent config
|
||||
copy:
|
||||
# Environment variables (uncomment and customize as needed)
|
||||
# - .env
|
||||
# - .env.local
|
||||
# Workflow config
|
||||
- .trellis/.developer
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Post-Create Hooks
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Commands to run after creating worktree
|
||||
# Executed in worktree directory, in order, abort on failure
|
||||
post_create:
|
||||
# Install dependencies (uncomment based on your package manager)
|
||||
# - npm install
|
||||
# - pnpm install --frozen-lockfile
|
||||
# - yarn install --frozen-lockfile
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Check Agent Verification (Ralph Loop)
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# Commands to verify code quality before allowing check agent to finish
|
||||
# If configured, Ralph Loop will run these commands - all must pass to allow completion
|
||||
# If not configured or empty, trusts agent's completion markers
|
||||
verify:
|
||||
# - pnpm lint
|
||||
# - pnpm typecheck
|
||||
22
AGENTS.md
Normal file
22
AGENTS.md
Normal file
@ -0,0 +1,22 @@
|
||||
<!-- TRELLIS:START -->
|
||||
# Trellis Instructions
|
||||
|
||||
These instructions are for AI assistants working in this project.
|
||||
|
||||
Use the `/trellis:start` command when starting a new session to:
|
||||
- Initialize your developer identity
|
||||
- Understand current project context
|
||||
- Read relevant guidelines
|
||||
|
||||
Use `@/.trellis/` to learn:
|
||||
- Development workflow (`workflow.md`)
|
||||
- Project structure guidelines (`spec/`)
|
||||
- Developer workspace (`workspace/`)
|
||||
|
||||
If you're using Codex, project-scoped helpers may also live in:
|
||||
- `.agents/skills/` for reusable Trellis skills
|
||||
- `.codex/agents/` for optional custom subagents
|
||||
|
||||
Keep this managed block so 'trellis update' can refresh the instructions.
|
||||
|
||||
<!-- TRELLIS:END -->
|
||||
Loading…
Reference in New Issue
Block a user