From f223f559ade7a1fafb64fd2f78320989c42e94bb Mon Sep 17 00:00:00 2001 From: shunli Date: Sun, 26 Jul 2026 18:10:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7=20Trellis=20=E5=B9=B6?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E9=A1=B9=E7=9B=AE=E5=AE=A1=E6=9F=A5=EF=BC=88?= =?UTF-8?q?07-26-trellis-upgrade-project-audit=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/trellis-before-dev/SKILL.md | 18 +- .agents/skills/trellis-brainstorm/SKILL.md | 604 ++------- .agents/skills/trellis-break-loop/SKILL.md | 58 + .agents/skills/trellis-channel/SKILL.md | 67 + .../references/command-reference.md | 480 +++++++ .../trellis-channel/references/forum.md | 233 ++++ .../references/progress-debugging.md | 226 ++++ .../trellis-channel/references/workers.md | 276 ++++ .../trellis-channel/references/workflows.md | 128 ++ .agents/skills/trellis-check/SKILL.md | 8 +- .agents/skills/trellis-continue/SKILL.md | 13 +- .agents/skills/trellis-meta/SKILL.md | 80 +- .../customize-local/change-agents.md | 2 + .../customize-local/change-context-loading.md | 11 +- .../customize-local/change-hooks.md | 2 +- .../change-skills-or-commands.md | 53 +- .../customize-local/change-spec-structure.md | 2 +- .../customize-local/change-workflow.md | 7 +- .../references/customize-local/overview.md | 4 +- .../local-architecture/bundled-skills.md | 147 +++ .../local-architecture/context-injection.md | 10 +- .../local-architecture/generated-files.md | 8 +- .../local-architecture/multi-agent-channel.md | 69 + .../references/local-architecture/overview.md | 2 +- .../local-architecture/spec-system.md | 2 +- .../local-architecture/task-system.md | 41 +- .../references/platform-files/agents.md | 14 +- .../platform-files/hooks-and-settings.md | 3 + .../references/platform-files/overview.md | 10 +- .../references/platform-files/platform-map.md | 45 +- .../platform-files/skills-and-commands.md | 7 +- .../skills/trellis-session-insight/SKILL.md | 81 ++ .../references/cli-quick-reference.md | 65 + .../references/triggering-patterns.md | 93 ++ .../SKILL.md | 4 +- .../references/mcp-setup.md | 0 .../references/repository-analysis.md | 0 .../references/spec-task-planning.md | 0 .../references/spec-writing.md | 0 .agents/skills/trellis-start/SKILL.md | 13 +- .codex/agents/trellis-check.toml | 69 +- .codex/agents/trellis-implement.toml | 69 +- .codex/agents/trellis-research.toml | 30 +- .codex/config.toml | 11 + .codex/hooks.json | 12 + .codex/hooks/inject-subagent-context.py | 1133 +++++++++++++++++ .codex/hooks/inject-workflow-state.py | 232 ++-- .codex/hooks/session-start.py | 320 +++-- .gitattributes | 9 + .trellis/.template-hashes.json | 116 +- .trellis/.version | 2 +- .trellis/agents/check.md | 70 + .trellis/agents/implement.md | 71 ++ .trellis/config.yaml | 62 +- .trellis/scripts/add_session.py | 216 +++- .trellis/scripts/common/active_task.py | 59 +- .trellis/scripts/common/cli_adapter.py | 217 +++- .trellis/scripts/common/config.py | 128 +- .trellis/scripts/common/git.py | 36 + .trellis/scripts/common/io.py | 32 +- .trellis/scripts/common/safe_commit.py | 60 +- .trellis/scripts/common/session_context.py | 2 +- .trellis/scripts/common/task_context.py | 116 +- .trellis/scripts/common/task_store.py | 320 ++++- .trellis/scripts/common/task_utils.py | 24 + .trellis/scripts/common/trellis_config.py | 3 +- .trellis/scripts/common/workflow_phase.py | 48 +- .trellis/scripts/task.py | 132 +- .trellis/spec/hyb-oa/project-governance.md | 81 ++ .../check.jsonl | 1 + .../design.md | 36 + .../implement.jsonl | 1 + .../implement.md | 41 + .../prd.md | 88 ++ .../research/current-state-audit.md | 95 ++ .../task.json | 35 + .trellis/workflow.md | 314 ++--- 77 files changed, 5850 insertions(+), 1327 deletions(-) create mode 100644 .agents/skills/trellis-channel/SKILL.md create mode 100644 .agents/skills/trellis-channel/references/command-reference.md create mode 100644 .agents/skills/trellis-channel/references/forum.md create mode 100644 .agents/skills/trellis-channel/references/progress-debugging.md create mode 100644 .agents/skills/trellis-channel/references/workers.md create mode 100644 .agents/skills/trellis-channel/references/workflows.md create mode 100644 .agents/skills/trellis-meta/references/local-architecture/bundled-skills.md create mode 100644 .agents/skills/trellis-meta/references/local-architecture/multi-agent-channel.md create mode 100644 .agents/skills/trellis-session-insight/SKILL.md create mode 100644 .agents/skills/trellis-session-insight/references/cli-quick-reference.md create mode 100644 .agents/skills/trellis-session-insight/references/triggering-patterns.md rename .agents/skills/{trellis-spec-bootstarp => trellis-spec-bootstrap}/SKILL.md (97%) rename .agents/skills/{trellis-spec-bootstarp => trellis-spec-bootstrap}/references/mcp-setup.md (100%) rename .agents/skills/{trellis-spec-bootstarp => trellis-spec-bootstrap}/references/repository-analysis.md (100%) rename .agents/skills/{trellis-spec-bootstarp => trellis-spec-bootstrap}/references/spec-task-planning.md (100%) rename .agents/skills/{trellis-spec-bootstarp => trellis-spec-bootstrap}/references/spec-writing.md (100%) create mode 100755 .codex/hooks/inject-subagent-context.py mode change 100644 => 100755 .codex/hooks/inject-workflow-state.py mode change 100644 => 100755 .codex/hooks/session-start.py create mode 100644 .gitattributes create mode 100644 .trellis/agents/check.md create mode 100644 .trellis/agents/implement.md create mode 100644 .trellis/tasks/07-26-trellis-upgrade-project-audit/check.jsonl create mode 100644 .trellis/tasks/07-26-trellis-upgrade-project-audit/design.md create mode 100644 .trellis/tasks/07-26-trellis-upgrade-project-audit/implement.jsonl create mode 100644 .trellis/tasks/07-26-trellis-upgrade-project-audit/implement.md create mode 100644 .trellis/tasks/07-26-trellis-upgrade-project-audit/prd.md create mode 100644 .trellis/tasks/07-26-trellis-upgrade-project-audit/research/current-state-audit.md create mode 100644 .trellis/tasks/07-26-trellis-upgrade-project-audit/task.json diff --git a/.agents/skills/trellis-before-dev/SKILL.md b/.agents/skills/trellis-before-dev/SKILL.md index 9c6ec9c7..5a4b8523 100644 --- a/.agents/skills/trellis-before-dev/SKILL.md +++ b/.agents/skills/trellis-before-dev/SKILL.md @@ -7,28 +7,34 @@ Read the relevant development guidelines before starting your task. Execute these steps: -1. **Discover packages and their spec layers**: +1. **Read current task artifacts**: + - `prd.md` for requirements and acceptance criteria + - `design.md` if present for technical design + - `implement.md` if present for execution order and validation plan + +2. **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: +3. **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.) + - Any spec/research paths referenced by the task artifacts -3. **Read the spec index** for each relevant module: +4. **Read the spec index** for each relevant module: ```bash cat .trellis/spec///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. **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**: +6. **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. +7. Understand the coding standards and patterns you need to follow, then proceed with your development plan. This step is **mandatory** before writing any code. diff --git a/.agents/skills/trellis-brainstorm/SKILL.md b/.agents/skills/trellis-brainstorm/SKILL.md index c843eb8c..096eeda1 100644 --- a/.agents/skills/trellis-brainstorm/SKILL.md +++ b/.agents/skills/trellis-brainstorm/SKILL.md @@ -3,546 +3,198 @@ name: trellis-brainstorm description: "Guides collaborative requirements discovery before implementation. Creates task directory, seeds PRD, asks high-value questions one at a time, researches technical choices, and converges on MVP scope. Use when requirements are unclear, there are multiple valid approaches, or the user describes a new feature or complex task." --- -# Brainstorm - Requirements Discovery (AI Coding Enhanced) +# Trellis Brainstorm -**CoreRule**: Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. +## Non-Negotiable Planning Contract -Ask the questions one at a time. +A request to build, implement, fix, refactor, or "go ahead" is not approval to leave planning. Task-creation consent is also not implementation approval. + +For every non-trivial task, the user must respond at least once after the initial request before implementation begins. If no clarification is needed, that response must approve the final planning summary described below. + +While any user-owned product, scope, UX, compatibility, risk, or acceptance decision remains unresolved, end the turn with exactly one highest-value question. Do not edit product code, dispatch implementation, or run `task.py start`. + +## Non-Negotiable Evidence Rule If a question can be answered by exploring the codebase, explore the codebase instead. ---- +This is mandatory. Before asking the user a question, first check whether the answer is already available in code, tests, configs, docs, existing specs, or task history. -Guide AI through collaborative requirements discovery **before implementation**, optimized for AI coding workflows: +Do not ask the user to confirm facts that the repository can answer. Ask only for product intent, preference, scope, risk tolerance, acceptance behavior, or decisions that remain ambiguous after inspection. -* **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) +Repository evidence establishes current behavior and technical constraints. The user's intended behavior, feature scope boundaries, and UX preferences are never answerable by repository evidence alone, even when an existing pattern exists; existing patterns are options and recommendation evidence, not decisions. --- -## When to Use +Use this skill during Phase 1 planning to turn the user's request into clear requirements and planning artifacts. -Triggered from `start` (Trellis command) when the user describes a development task, especially when: +## Preconditions -* 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 +Use this skill only after task-creation consent has been given and the user is ready to enter Trellis planning. ---- - -## 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. +If no task exists yet, create one: ```bash -TASK_DIR=$(python3 ./.trellis/scripts/task.py create "brainstorm: " --slug ) +TASK_DIR=$(python3 ./.trellis/scripts/task.py create "" --slug ) ``` -Use a slug without a date prefix. `task.py create` adds the `MM-DD-` -directory prefix automatically. +Use a concise title from the user's request. Use a slug without a date prefix. `task.py create` adds the `MM-DD-` directory prefix automatically. -Create/seed `prd.md` immediately with what you know: +`task.py create` creates the default `prd.md`. Update that file with the current understanding before asking follow-up questions. -```markdown -# brainstorm: +## Planning Flow -## Goal +1. Capture the user's request and initial known facts in `prd.md`. +2. Inspect available evidence before asking questions: + - code, tests, fixtures, and configs + - README files, docs, existing specs, and domain notes + - related Trellis tasks, research files, and session history when present +3. Separate what you found into: + - confirmed facts + - product intent still needed from the user + - scope or risk decisions still needed from the user + - likely out-of-scope items +4. If a user-owned decision remains, ask the single highest-value question, include your recommendation and trade-off, then stop. Do not perform implementation work in the same turn. +5. After each user answer, update `prd.md`, recompute the decision inventory, and repeat from step 2. +6. When no user-owned decision remains, create or update `design.md` and `implement.md` for complex tasks. +7. Run the requirement convergence gate, then the PRD convergence pass. +8. Present the final planning summary and stop. Do not run `task.py start` or edit product code in the same turn. +9. Only a subsequent user message that explicitly approves the latest planning summary authorizes `task.py start` and implementation. If the artifacts change materially after approval, repeat the final review. - +Do not invent a project-specific product/spec hierarchy. If the repository already has product, domain, or spec docs, use them. If it does not, proceed with the evidence that exists. -## What I already know +## Question Rules -* -* +Ask only one question per message. -## Assumptions (temporary) +Each question must include: -* +- the decision needed +- why the answer matters +- your recommended answer +- the trade-off if the user chooses differently -## Open Questions +Do not ask process questions such as whether to search, inspect files, or continue brainstorming. Do the evidence work directly. Ask the user only when the remaining issue is a product decision, preference, scope boundary, or risk tolerance choice. -* +Recommendations are not default selections. Never choose a recommended product decision on the user's behalf merely because the user asked for implementation. -## Requirements (evolving) +Do not manufacture clarification questions when the request and repository evidence already resolve every decision. In that case, proceed directly to the final planning summary, which still requires a subsequent explicit approval. -* +The final review is a required phase-transition gate, not a prohibited process question. Task-creation consent, the initial implementation request, and approval given before the latest final summary do not satisfy this gate. -## Acceptance Criteria (evolving) +## Thinking Framework: First Principles Analysis -* [ ] +When requirements are vague, solutions feel over-engineered, or you're about to add complexity "because everyone does" — decompose to fundamental truths before reasoning upward. -## Definition of Done (team quality bar) +### Step 1: Restate the Problem -* Tests added/updated (unit/integration where appropriate) -* Lint / typecheck / CI green -* Docs/notes updated if behavior changes -* Rollout/rollback considered if risky +Strip away implementation details to one sentence. -## Out of Scope (explicit) +> Bad: "We need to add Redis caching to the user profile endpoint" +> Good: "User profile data takes too long to load" -* +### Step 2: List Fundamental Truths -## Technical Notes +What is absolutely true (not opinion or convention)? -* -* -``` +| Category | Examples | +|----------|----------| +| **Physical constraints** | Network latency ≥ 0, disk I/O has limits | +| **Business rules** | "Users must see their own data" | +| **Technical invariants** | "Data must be consistent" | +| **User needs** | "The user wants X within Y seconds" | ---- +### Step 3: Challenge Assumptions -## Step 1: Auto-Context (DO THIS BEFORE ASKING QUESTIONS) +For each component of the current plan: -Before asking questions like "what does the code look like?", gather context yourself: +- **Fact or convention?** "We always use REST" — why? +- **What if we removed this?** If nothing breaks, it's unnecessary. +- **Solving the actual problem or a symptom?** Trace the causal chain. +- **Who benefits from this complexity?** If "nobody", simplify. -### Repo inspection checklist +### Step 4: Build Up from Truths -* 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) +1. Start with the minimum viable mechanism satisfying all truths +2. Add complexity only when a specific truth demands it +3. Each addition must answer: "Which truth requires this?" -### Documentation checklist +### Step 5: Validate -* Look for existing PRDs/specs/templates -* Look for command usage examples, README, ADRs if any +- Does the solution solve the original problem? +- What assumptions need verification? +- What's the simplest experiment to test this? -Write findings into PRD: +## Requirement Convergence Gate -* Add to `What I already know` -* Add constraints/links to `Technical Notes` +Before final review, verify all of the following: ---- +- the user outcome and product value are explicit +- in-scope and out-of-scope behavior are explicit +- acceptance criteria describe observable outcomes +- user-owned product, scope, UX, compatibility, and risk decisions are resolved +- blocking open questions are empty +- technical unknowns are researched or explicitly deferred without changing MVP behavior -## Step 2: Classify Complexity (still useful, not gating task creation) +Lightweight tasks may omit `design.md` and `implement.md`; they may not skip evidence inspection, requirement convergence, final review, or fresh implementation approval. -| 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 | +The final planning summary must show Goal, In Scope, Out of Scope, Acceptance Criteria, Key Decisions, relevant Risks or Deferred Items, and artifact status. -> Note: Task already exists from Step 0. Classification only affects depth of brainstorming. +## Artifact Rules ---- +`prd.md` records requirements and acceptance: -## Step 3: Question Gate (Ask ONLY high-value questions) +- goal and user value +- confirmed facts +- requirements +- acceptance criteria +- out of scope +- open questions that still block planning -Before asking ANY question, run the following gate: +`design.md` records technical design for complex tasks: -### Gate A — Can I derive this without the user? +- architecture and boundaries +- data flow and contracts +- compatibility and migration notes +- important trade-offs +- operational or rollback considerations -If answer is available via: +`implement.md` records execution planning for complex tasks: -* repo inspection (code/config) -* docs/specs/conventions -* quick market/OSS research +- ordered implementation checklist +- validation commands +- risky files or rollback points +- follow-up checks before `task.py start` -→ **Do not ask.** Fetch it, summarize, update PRD. +Lightweight tasks may have only `prd.md`. Complex tasks must have `prd.md`, `design.md`, and `implement.md` before `task.py start`. -### Gate B — Is this a meta/lazy question? +`implement.md` is not a replacement for `implement.jsonl`. On sub-agent-dispatch workflows, `implement.jsonl` and `check.jsonl` must each contain at least one real spec/research entry before `task.py start`; the seed `_example` row does not count. Inline workflows skip this JSONL gate because Phase 2 loads context through `trellis-before-dev`. -Examples: +## PRD Convergence Pass -* "Should I search?" -* "Can you paste the code so I can proceed?" -* "What does the code look like?" (when repo is available) +Before declaring planning ready or running `task.py start`, rewrite `prd.md` once against the final structure described in the artifact rules above. This is not optional cleanup; it is the final planning gate. -→ **Do not ask.** Take action. +The pass must be lossless: -### Gate C — What type of question is it? +- Collapse repeated facts into one authoritative section. +- Fold temporary brainstorm sections such as `What I already know`, `Assumptions`, and resolved `Open Questions` into Goal, Background, Requirements, Technical Notes, or Acceptance Criteria. +- Remove resolved open questions instead of leaving empty or already-answered sections. +- Merge parallel bug and requirement lists when they describe the same work; keep each defect's severity, evidence, and file:line anchors on the owning requirement. +- Preserve every file:line anchor, decision, constraint, requirement ID, and acceptance-criteria mapping. +- Do not proceed to final review while any blocking open question remains. -* **Blocking**: cannot proceed without user input -* **Preference**: multiple valid choices, depends on product/UX/risk preference -* **Derivable**: should be answered by inspection/research +After the pass, read `prd.md` top to bottom and verify that no fact is repeated across sections unless the repetition adds new information. -→ Only ask **Blocking** or **Preference**. +## Quality Bar ---- +Before declaring planning ready: -## Step 4: Research-first Mode (Mandatory for technical choices) +- `prd.md` contains testable acceptance criteria. +- `prd.md` has passed the PRD convergence pass: no unresolved temporary brainstorm sections, no duplicate facts across sections, and no lost anchors, decisions, or acceptance mappings. +- Repository-answerable questions have already been answered through inspection. +- Blocking open questions are empty. +- Complex tasks have `design.md` and `implement.md`. +- Sub-agent-dispatch tasks have real curated entries in both `implement.jsonl` and `check.jsonl`; seed-only manifests are not ready. +- The latest final planning summary has been presented to the user. +- In a subsequent message, the user explicitly approved that summary for implementation. -### Trigger conditions (any → research-first) - -* The task involves selecting an approach, library, protocol, framework, template system, plugin mechanism, or CLI UX convention -* The user asks for "best practice", "how others do it", "recommendation" -* The user can't reasonably enumerate options - -### Delegate to `trellis-research` sub-agent (don't research inline) - -For each research topic, **spawn a `trellis-research` sub-agent via the Task tool** — don't do WebFetch / WebSearch / `gh api` inline in the main conversation. - -Why: -- The sub-agent has its own context window → doesn't pollute brainstorm context with raw tool output -- It persists findings to `{TASK_DIR}/research/.md` (the contract — see `workflow.md` Phase 1.2) -- It returns only `{file path, one-line summary}` to the main agent -- Independent topics can be **parallelized** — spawn multiple sub-agents in one tool call - -> **Codex exception**: on Codex CLI, do NOT dispatch `trellis-research` for research-first mode — do the research inline (WebFetch / WebSearch in the main session) and write findings to `{TASK_DIR}/research/.md` yourself. Reason: Codex `spawn_agent` runs sub-agents with `fork_turns="none"` (isolated context, no parent session inheritance), so the research sub-agent cannot resolve the active task path via `task.py current` and silently aborts without producing files. Inline research on Codex avoids this failure mode. The 3+ inline research calls limit (B rule in `workflow.md`) is relaxed for Codex specifically. - -Agent type: `trellis-research` -Task description template: "Research ; persist findings to `{TASK_DIR}/research/.md`." - -❌ Bad (what you must NOT do): -``` -Main agent: WebFetch(url-A) → WebFetch(url-B) → Bash(gh api ...) - → WebSearch(q1) → WebSearch(q2) → ... (10+ inline calls) - → Write(research/topic.md) -``` -→ Pollutes main context with raw HTML/JSON, burns tokens. - -✅ Good: -``` -Main agent: Task(subagent_type="trellis-research", - prompt="Research topic A; persist to research/topic-a.md") - + Task(subagent_type="trellis-research", - prompt="Research topic B; persist to research/topic-b.md") - + Task(subagent_type="trellis-research", - prompt="Research topic C; persist to research/topic-c.md") -→ Reads research/topic-{a,b,c}.md after they finish. -``` - -### Research steps (to pass into each sub-agent prompt) - -Each `trellis-research` sub-agent should: - -1. Identify 2–4 comparable tools/patterns for its topic -2. Summarize common conventions and why they exist -3. Map conventions onto our repo constraints -4. Write findings to `{TASK_DIR}/research/.md` - -Main agent then reads the persisted files and produces **2–3 feasible approaches** in PRD. - -### Research output format (PRD) - -The PRD itself should only reference the persisted research files, not duplicate their content. Add a `## Research References` section pointing at `research/*.md`. - -Optionally, add a convergence section with feasible approaches derived from the research: - -```markdown -## Research References - -* [`research/.md`](research/.md) — -* [`research/.md`](research/.md) — - -## Research Notes - -### What similar tools do - -* ... -* ... - -### Constraints from our repo/project - -* ... - -### Feasible approaches here - -**Approach A: ** (Recommended) - -* How it works: -* Pros: -* Cons: - -**Approach B: ** - -* How it works: -* Pros: -* Cons: - -**Approach C: ** (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: . - -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 (reserve for future extension) -3. Add (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 , which approach do you prefer? - -1. **Option A** — -2. **Option B** — -3. **Option C** — -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: ** (Recommended) - -* How: -* Pros: -* Cons: - -**Approach B: ** - -* 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**: - -**Requirements**: - -* ... -* ... - -**Acceptance Criteria**: - -* [ ] ... -* [ ] ... - -**Definition of Done**: - -* ... - -**Out of Scope**: - -* ... - -**Technical Approach**: - - -**Implementation Plan (small PRs)**: - -* PR1: -* PR2: -* PR3: - -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 -# - -## Goal - - - -## Requirements - -* ... - -## Acceptance Criteria - -* [ ] ... - -## Definition of Done - -* ... - -## Technical Approach - - - -## Decision (ADR-lite) - -Context / Decision / Consequences - -## Out of Scope - -* ... - -## Technical 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 | -|---------|-------------| -| ``start` (Trellis command)` | Entry point that triggers brainstorm | -| ``finish-work` (Trellis command)` | After implementation is complete | -| ``update-spec` (Trellis command)` | If new patterns emerge during work | +Do not start implementation merely because the user originally asked for implementation. diff --git a/.agents/skills/trellis-break-loop/SKILL.md b/.agents/skills/trellis-break-loop/SKILL.md index ef2b50cd..1c8b3974 100644 --- a/.agents/skills/trellis-break-loop/SKILL.md +++ b/.agents/skills/trellis-break-loop/SKILL.md @@ -111,6 +111,64 @@ Three levels of insight: 30 minutes of analysis saves 30 hours of future debugging. +## Thinking Framework: Bayesian Reasoning + +When multiple root causes are plausible and evidence is incomplete, update your beliefs proportionally to new evidence rather than clinging to initial assumptions. + +### Step 1: Establish Priors + +Before investigating, state what you believe and why: + +| Hypothesis | Prior | Reasoning | +|------------|-------|-----------| +| H1: [cause A] | 40% | Most common for this pattern | +| H2: [cause B] | 30% | Plausible given environment | +| H3: [other] | 30% | Catch-all | + +Priors must sum to 100%. If you can't assign probabilities, investigate first. + +### Step 2: Observe Evidence + +Document what you found — be specific about reliability: + +- What exactly did you observe? +- How reliable? (test output > log message > user report > hunch) +- Could multiple hypotheses explain this? + +### Step 3: Update Beliefs + +For each hypothesis, ask: **How likely is this evidence if this hypothesis were true?** + +Direction of update matters more than calculation: +- Evidence strongly predicted by H1 → H1 probability increases +- Evidence contradicts H2 → H2 probability decreases +- Evidence equally likely under all → no update + +### Step 4: Seek Discriminating Evidence + +Don't gather more of the same. Find evidence that **differs strongly** between top hypotheses. + +> If H1 and H3 are close: "What would I see if H1 is true but not if H3 is true?" Then check for that. + +### Step 5: State Confidence + +| Confidence | Action | +|------------|--------| +| 90%+ | Proceed with fix, monitor | +| 70-90% | Proceed, add fallback check | +| 50-70% | Test hypothesis before committing | +| <50% | Need more evidence, don't guess | + +Never express binary certainty when evidence is incomplete. Use "most likely", "plausible but unlikely", "worth investigating". + +### Common Fallacies + +| Fallacy | Example | Correction | +|---------|---------|------------| +| **Base rate neglect** | "Test failed → code is broken" | How often do tests fail for other reasons? | +| **Confirmation bias** | "Must be a race condition, let me find race evidence" | Actively seek evidence AGAINST your top hypothesis | +| **Anchoring** | "Last time it was caching, probably caching again" | Establish priors from current context, not yesterday's bug | + --- ## After Analysis: Immediate Actions diff --git a/.agents/skills/trellis-channel/SKILL.md b/.agents/skills/trellis-channel/SKILL.md new file mode 100644 index 00000000..511ee026 --- /dev/null +++ b/.agents/skills/trellis-channel/SKILL.md @@ -0,0 +1,67 @@ +--- +name: trellis-channel +description: Use Trellis channel for live multi-agent collaboration, spawned workers, cross-agent review, progress inspection, forum channels, and channel log debugging. +--- + +# trellis-channel + +`trellis channel` is the local multi-agent collaboration runtime. Reach for it when agents need to talk through a durable event log, when a worker should be spawned as a peer process, when an in-flight worker needs interrupt / debugging, or when feedback should be recorded on a durable `--type forum` channel. + +Typical user signals: "和 codex/claude 讨论", "brainstorm with another agent", "spawn an implement/check worker", "let agent review", "open an issue board / changelog forum", "look at this thread", "channel is stuck / no output", "progress was truncated", "how do I write that channel command". + +This skill is an index. Load only the reference file for the current job — do not preload all of them. + +## First Commands + +```bash +trellis --version +trellis channel --help +trellis channel list --all +trellis channel list --scope global --all +``` + +If the user names a channel or thread, inspect it before asking for background: + +```bash +trellis channel forum --scope global +trellis channel thread --scope global +trellis channel context list --scope global --thread +``` + +## Route By User Intent + +| User intent | Read | +|---|---| +| "和 codex/claude 讨论一下", "brainstorm with another agent" | `references/workflows.md` | +| "派一个 implement/check agent", "让 agent review", "spawn a worker" | `references/workflows.md`, then `references/workers.md` | +| "开 issue 区 / topic 群 / changelog / board", "make a forum" | `references/forum.md` | +| "看看这个 thread / linked context", "inspect a thread" | `references/forum.md` | +| "channel 卡住了 / 没输出 / progress 被截断", "worker stalled" | `references/progress-debugging.md` | +| "具体命令怎么写", "what flags does X take" | `references/command-reference.md` | + +## Core Rules + +- New forum channels use `--type forum`. A `thread` is one item inside a forum channel. +- Use `--context-file` / `--context-raw` and `trellis channel context add/delete/list`. `--linked-context-*` is deprecated terminology. +- Use `--stdin` or `--text-file` for long messages. Do not put long mixed Chinese/English text in the positional shell argument. +- Pretty `messages` output is an operator dashboard and may truncate progress. Use `--raw` for audit. +- `--as` is the speaker or worker handle, depending on the command. Use explicit, stable names when multiple agents or sessions are involved. +- `--scope project` (default) operates on the current cwd's project bucket; `--scope global` operates on the shared `__global__` bucket. Pick scope deliberately — a global board is invisible from project listings unless `--scope global` is passed. +- For brainstorm, do multiple pressure-test rounds. One answer plus one confirmation is review, not brainstorm. +- **Dispatcher wait pattern**: use `--kind done` / `--kind turn_finished` (trellis-emitted system events), NOT a user `--tag` as the completion signal. CLI help lists `phase_done` / `question` as `--tag` examples but only `interrupt` is a reserved tag with hardcoded trellis behavior; the others are opaque user labels. Relying on a worker to run `send --tag ` is unreliable — LLM workers commonly write the tag string into prose instead of running the actual CLI command. See `references/command-reference.md` "tag vs kind". +- Forum channels are event-sourced. Do not parse `events.jsonl` first; use `forum`, `thread`, `messages --thread`, and `context list`. +- `@mindfoldhq/trellis-core` owns reusable channel/thread state, event append, seq allocation, context/title projection, reducers, and task helpers. The CLI owns flags, terminal rendering, prompts, worker lifecycle, and process exits. + +## Reference Files + +- `references/workflows.md` — canonical collaboration patterns A–F (peer brainstorm, spawned review, dispatch-and-wait, forum issue capture, interrupt-and-redirect, one-shot run). +- `references/forum.md` — forum channels, context, title, rename, changelog forums, thread filtering. +- `references/workers.md` — spawn, agent cards, context injection (`--file` / `--jsonl`), interrupts, kill semantics. +- `references/progress-debugging.md` — progress/raw inspection, stalled worker diagnosis, OOM guard, exit codes. +- `references/command-reference.md` — current CLI command reference (every subcommand, every flag, output conventions, scope/type model). + +## Not For + +- One static review where a markdown file and prompt are enough. +- Replacing normal tool calls with self-logging. +- Long-term memory retrieval. Use durable forum channels for actionable issues, and `trellis mem` (the `trellis-session-insight` skill) for session/history search. diff --git a/.agents/skills/trellis-channel/references/command-reference.md b/.agents/skills/trellis-channel/references/command-reference.md new file mode 100644 index 00000000..75def26f --- /dev/null +++ b/.agents/skills/trellis-channel/references/command-reference.md @@ -0,0 +1,480 @@ +# Command Reference + +Authoritative current command reference for `trellis channel` subcommands, +validated against the source in `packages/cli/src/commands/channel/` +(`index.ts` Commander wiring and each subcommand handler). + +Every subcommand accepts `--scope ` unless noted; `project` +is the default and resolves against the current cwd's project bucket. + +## Top-level + +``` +trellis channel +``` + +> Multi-agent collaboration runtime — spawn / coordinate / interrupt worker +> agents through a shared event log. + +--- + +## Create / List + +### `create ` + +```bash +trellis channel create + [--scope project|global] # default: project + [--type chat|forum] # default: chat + [--task ] # associated Trellis task dir + [--project ] + [--labels a,b,c] + [--description ] # stable channel description + [--context-file ] ... # repeatable + [--context-raw ] ... # repeatable + [--linked-context-file ] # [deprecated alias] + [--linked-context-raw ] # [deprecated alias] + [--cwd ] # recorded in create event + [--by ] # default: main + [--force] # overwrite existing channel + [--ephemeral] # hide from default list, prunable +``` + +Behavior: +- Appends a `create` event; immutable `type` (cannot mutate forum↔chat after). +- `--ephemeral` channels are hidden from `channel list` by default and are + the sweep target for `channel prune --ephemeral`. +- `--linked-context-*` are folded into `--context-*`; emit a deprecation + notice when used. + +### `list` + +```bash +trellis channel list + [--scope project|global] + [--json] + [--project ] # substring match on task field + [--all] # include ephemeral (suffix '*') + [--all-projects] # scan every project bucket +``` + +Behavior: +- Default scope: current cwd's project. `--all-projects` scans every bucket. +- Pretty mode prints `NAME WORKERS EVENTS LAST KIND TYPE TASK`, sorted by + recency, with a footer noting hidden ephemeral count. +- `--json` switches to a JSON array. + +--- + +## Chat Messages + +### `send [text]` + +```bash +trellis channel send [text] + --as # REQUIRED — author + [--scope project|global] + [--to ] # default: broadcast + [--stdin | --text-file ] # body from stdin or file + [--delivery-mode appendOnly|requireKnownWorker|requireRunningWorker] +``` + +Behavior: +- Body precedence: positional `[text]` → `--stdin` → `--text-file`. +- `--to` with one entry stores a string; multiple stores an array; omitted + means broadcast. +- `--delivery-mode` selects targeted-delivery validation: + - `appendOnly` (default-ish — just record), + - `requireKnownWorker` (the named target must have a `spawned` event), + - `requireRunningWorker` (the worker must currently be live). +- Prints the appended event as one JSON line on stdout. + +> **Note:** `send` has **no** `--tag` and **no** `--kind` flag. See +> [`tag-vs-kind`](#tag-vs-kind--how-event-shape-is-actually-controlled) below. + +### `messages ` + +```bash +trellis channel messages + [--scope project|global] + [--raw] # one JSON event per line + [--follow] # stream new events + [--last ] # last N matching events + [--since ] # seq > N + [--kind ] # one of CHANNEL_EVENT_KINDS + [--from ] # author filter + [--to ] # routing target filter + [--thread ] # forum-only + [--action ] # forum-only + [--no-progress] # hide progress events +``` + +Behavior: +- Auto-detects forum channels: with no filters it renders the thread board + instead of the event stream. `--thread` / `--action` are forum-only and + error against chat channels. +- `--kind` is validated against `CHANNEL_EVENT_KINDS` (single value, not + CSV — that's the `wait` side). + +### `wait ` + +```bash +trellis channel wait + --as # REQUIRED — self for filter ctx + [--scope project|global] + [--timeout ] # parsed by parseDuration + [--from ] # author CSV + [--kind ] # CSV, OR semantics + [--thread ] # forum filter + [--action ] # forum filter + [--to ] # default: own agent (broadcast + me) + [--include-progress] # also wake on progress events + [--all] # require every --from to match +``` + +Behavior: +- Streams matching events as JSON, one per line. +- Default `--to` filter is the caller's own agent (broadcast events still + match — broadcast + explicit-to-me). +- `--all` requires `--from` and blocks until every listed agent has produced + a matching event. +- **Timeout exits 124** and prints `timeout: still waiting on ...` to stderr + when `--all` was in play. + +--- + +## tag-vs-kind — how event shape is actually controlled + +There is **no `--tag` flag** anywhere in the v0.6.0 channel CLI; `--kind` is +not a legacy alias for any `--tag` flag. + +Concrete model in the current source: + +- `--kind` is the only event-type filter, and it is constrained to the + trellis-emitted whitelist (`CHANNEL_EVENT_KINDS` in + `packages/core/src/channel/internal/store/events.ts`): + - `create`, `join`, `leave`, `message`, `thread`, `context`, `channel`, + `spawned`, `killed`, `respawned`, `progress`, `done`, `error`, + `waiting`, `awake`, `undeliverable`, `interrupt_requested`, + `turn_started`, `turn_finished`, `interrupted`, `supervisor_warning` + - Passing anything else throws + `Invalid --kind ''. Must be one of: …`. +- `--kind` lives on `wait` (CSV, OR semantics) and `messages` (single + value). `send` and `run` cannot emit a custom kind — every `send` writes + a `message` event. +- Mid-turn worker abort is **not** a tag. It is the dedicated + `channel interrupt` command, which appends an `interrupt_requested` / + `interrupted` pair and provider-level interrupts the worker. + +Practical rule for dispatchers waiting on workers: + +- Use `--kind done,turn_finished` for "worker finished a turn" — these are + system events that the supervisor fires automatically. Do not depend on + the worker LLM remembering to emit any custom signal. +- Use `trellis channel interrupt` (the command) only when you actually want + mid-turn abort behavior. +- Do **not** invent user-side tags as completion signals. There is no + `--tag` filter; a worker writing a custom string into its final message + is just text inside a `message` event and cannot be matched by `wait`. + +Long bodies always go through stdin or a file: + +```bash +trellis channel send T --as A --stdin < /tmp/message.md +trellis channel send T --as A --text-file /tmp/message.md +``` + +--- + +## Interrupt + +### `interrupt [text]` + +```bash +trellis channel interrupt [text] + --as # REQUIRED — caller + --to # REQUIRED — target worker + [--scope project|global] + [--stdin | --text-file ] +``` + +Behavior: +- Appends an `interrupt` event with `reason: "user"` and a replacement + instruction body; supervisor performs provider-level interrupt where + supported (Claude `/interrupt`, Codex turn cancel). +- Prints the appended event JSON on stdout. + +--- + +## Workers + +### `spawn ` + +```bash +trellis channel spawn + [--scope project|global] + [--agent ] # loads .trellis/agents/.md + [--provider claude|codex] # overrides agent file + [--as ] # default: agent name + [--cwd ] + [--model ] + [--resume ] # session/thread id resume + [--timeout ] # auto-kill after duration + [--warn-before ] # supervisor_warning lead time + # default 5m, 0ms disables + [--file ] ... # glob, repeatable; inject content + [--jsonl ] ... # Trellis manifest, repeatable + [--by ] # spawn-event author + # default: TRELLIS_CHANNEL_AS env or 'main' + [--inbox-policy explicitOnly|broadcastAndExplicit] + # default explicitOnly + [--idle-timeout ] # OOM-guard idle TTL + # default 5m, 0 disables + [--max-live-workers ] # spawn-time live-worker budget + # default 6, 0 disables +``` + +Behavior: +- Provider is validated against the adapter registry + (`packages/cli/src/commands/channel/adapters/`); current: `claude`, + `codex`. +- Worker stays inbox-idle until the first `send --to `. +- Records a `spawned` event with `pid`, `provider`, `agent`, `files`, + `manifests`. +- OOM-guard precedence: CLI flag → env var + (`TRELLIS_CHANNEL_WORKER_IDLE_TIMEOUT`, + `TRELLIS_CHANNEL_MAX_LIVE_WORKERS`) → + `.trellis/config.yaml#channel.worker_guard` → built-in defaults. + +### `run [name]` + +```bash +trellis channel run [name?] + [--agent ] + [--provider claude|codex] + [--as ] + [--cwd ] + [--model ] + [--file ] ... # repeatable, glob + [--jsonl ] ... # repeatable + [--message | --message-file | --stdin] + [--timeout ] # default 5m +``` + +Behavior: +- One-shot. Auto-generates `run-` if `name` omitted. +- Creates an ephemeral channel (`createMode=run`), spawns a single worker, + sends the prompt, waits for `done`, prints the final assistant text to + stdout, then removes the channel on success. On failure the channel is + kept for inspection and exit code is 1. + +> `run` has **no** `--tag` flag. Completion is detected via the `done` +> event the supervisor emits. + +### `kill ` + +```bash +trellis channel kill + --as # REQUIRED — worker agent name + [--scope project|global] + [--force] # SIGKILL immediately +``` + +Behavior: +- Default path: SIGTERM → 8 s grace → SIGKILL escalation; the CLI writes a + `killed` event when SIGKILL was needed so the log stays truthful. +- Cleans `pid`, `worker-pid`, `config`, `spawnlock` sidecar files; keeps + `log`, `session-id`, `thread-id` for forensics / resume. + +### `rm ` + +```bash +trellis channel rm + [--scope project|global] +``` + +Behavior: +- Kills any live workers, then deletes the entire channel directory. +- Prints `Removed channel ''`. + +### `prune` + +```bash +trellis channel prune + [--scope project|global] # omitted: scan every project + [--all | --empty | --idle | --ephemeral] # mutually exclusive + [--yes] # actually delete (default: dry-run) + [--dry-run] # default true; redundant with default + [--keep ] # exclusion list +``` + +Behavior: +- Filter flags are mutually exclusive — error otherwise. +- Default is dry-run; `--yes` flips to real delete. +- Without `--scope`, scans **every** project bucket (intentional, repo-wide + cleanup); with `--scope project|global`, limited to that bucket. +- Live-worker channels are always skipped regardless of filter. +- Output: per-candidate line `name last-ts (reason)` plus a final summary. + +--- + +## Forum Channels + +### `post ` + +```bash +trellis channel post + --as # REQUIRED + [--scope project|global] + [--thread ] # required except action=opened + [--title ] + [--text | --stdin | --text-file ] + [--description ] # stable thread description + [--status ] + [--labels a,b] # REPLACES thread labels + [--assignees a,b] # REPLACES assignees + [--summary ] + [--context-file ] ... + [--context-raw ] ... + [--linked-context-file ] # [deprecated alias] + [--linked-context-raw ] # [deprecated alias] +``` + +Behavior: +- `` is free-form on the CLI surface; conventional values include + `opened`, `comment`, `status`, `labels`, `assignees`, `summary`, + `processed`. +- `action=rename` is rejected — use `thread rename` instead. +- `--labels` / `--assignees` are replace-semantics, not append. +- Output: appended event JSON on stdout. + +### `forum ` + +```bash +trellis channel forum + [--scope project|global] + [--status ] + [--raw] +``` + +Behavior: +- Lists threads (reduced state). `--status` filters by current thread + status. `--raw` prints one JSON per thread. + +### `thread ` / `thread rename` + +```bash +trellis channel thread + [--scope project|global] + [--raw] + +trellis channel thread rename + --as # REQUIRED + [--scope project|global] +``` + +Behavior: +- `thread ` shows one thread's timeline: + header ` [] `, then description / labels / + assignees / summary / timeline lines. `--raw` switches to raw events. +- `thread rename` is the only mutation; `post --action rename` is rejected. + +--- + +## Context / Title + +### `context add` / `context delete` / `context list` + +```bash +trellis channel context add <name> + [--as <agent>] # default: main + [--scope project|global] + [--thread <key>] # thread-level instead of channel-level + [--file <abs-path>] ... # repeatable + [--raw <text>] ... # repeatable + # at least one of --file or --raw + +trellis channel context delete <name> + [--as <agent>] # default: main + [--scope project|global] + [--thread <key>] + [--file <abs-path>] ... + [--raw <text>] ... + +trellis channel context list <name> + [--scope project|global] + [--thread <key>] + [--raw] # one JSON entry per line +``` + +Behavior: +- `add` / `delete` append a `context` event and print the event JSON. +- `list` projects current context entries; pretty output is + `file <path>` / `raw <truncated text>` lines, `(no context)` when empty. + +### `title set <name>` / `title clear <name>` + +```bash +trellis channel title set <name> + --title <text> # REQUIRED + [--as <agent>] # default: main + [--scope project|global] + +trellis channel title clear <name> + [--as <agent>] # default: main + [--scope project|global] +``` + +Behavior: +- Appends a `title` event projecting a stable display title onto the + channel. Output: event JSON. + +--- + +## Hidden / Internal + +| Command | Purpose | +|---|---| +| `channel __supervisor <channel> <worker> <config>` | Forked entry point invoked by `spawn`. Do not invoke directly. | +| `channel __parse-trace <adapter> <file>` | Dev helper — replays a recorded stream-json / wire trace through the matching adapter and prints the resulting channel events. Adapter is validated against the provider registry. | + +--- + +## Event Model + +`CHANNEL_EVENT_KINDS` (whitelist enforced by `parseChannelKind`): + +`create`, `join`, `leave`, `message`, `thread`, `context`, `channel`, +`spawned`, `killed`, `respawned`, `progress`, `done`, `error`, `waiting`, +`awake`, `undeliverable`, `interrupt_requested`, `turn_started`, +`turn_finished`, `interrupted`, `supervisor_warning`. + +`MEANINGFUL_EVENT_KINDS` (default-visible subset used by `wait` / +`messages` when no explicit `--kind` is given): + +`create`, `join`, `leave`, `message`, `thread`, `context`, `channel`, +`spawned`, `killed`, `respawned`, `done`, `error`. + +Non-meaningful kinds (e.g. `progress`, `waiting`, `awake`, +`supervisor_warning`, the `turn_*` / `interrupt*` set) still flow through +the store; opt in via `--kind` or `--include-progress`. + +Forum channels are event-sourced; use the CLI reducers +(`forum`, `thread`, `context list`) for state projection. + +--- + +## Output Conventions + +- **Mutations** (`send`, `interrupt`, `post`, `context add/delete`, + `title set/clear`, `thread rename`) print the appended event as one JSON + line on **stdout**. +- **Streaming reads** (`wait`, `messages --follow`) print one JSON event + per line on stdout. +- **Pretty reads** (`list`, `messages`, `forum`, `thread`, `context list`) + print colored, padded tables / timelines. +- **`run`** prints only the final assistant text on stdout (so callers can + pipe); diagnostic notes go to stderr. +- **Errors** go through `chalk.red("Error:")` to stderr and `exit 1`. +- **`wait` timeout** specifically exits **124**. + diff --git a/.agents/skills/trellis-channel/references/forum.md b/.agents/skills/trellis-channel/references/forum.md new file mode 100644 index 00000000..06b7f368 --- /dev/null +++ b/.agents/skills/trellis-channel/references/forum.md @@ -0,0 +1,233 @@ +# Forum Channels + +Forum channels are durable, topic-style channels. They are created with +`--type forum` at channel-creation time and are immutable after that. They are +not normal chat streams: the default read path is +**forum summary -> one thread timeline -> current context**. + +## Forum vs Regular Channel + +A channel's type is set with `--type` on `channel create` and never changes: + +- `chat` (default) — flat message timeline. `channel messages` always renders + the event stream. Forum-only flags such as `--thread` and `--action` are + rejected here. +- `forum` — thread-oriented. `channel messages` without filters renders a + thread-board summary instead of raw events. The `post`, `forum`, `thread`, + and `thread rename` subcommands only apply to forum channels. + +Both types share the same scope model (`--scope project` is the default; +`--scope global` puts the channel in the cross-project bucket). + +## Create A Forum Channel + +```bash +trellis channel create design-feedback \ + --type forum \ + --scope global \ + --description "Cross-project design feedback board." \ + --context-raw "One thread per design topic; close when resolved." \ + --by main +``` + +Use `--scope project` for a board scoped to one repo, `--scope global` for a +cross-project board. + +## Threads: Open, Comment, Status, Summary + +Threads live inside a forum channel. Each thread is identified by a stable +`--thread <key>` (lowercase kebab-case is conventional). The first action on +a thread is `opened`; everything afterwards uses the same `--thread` key. + +```bash +trellis channel post design-feedback opened \ + --scope global \ + --as main \ + --thread login-empty-state \ + --title "Empty state on the login screen" \ + --description "Track design feedback for the new login empty state." \ + --labels design,login \ + --context-raw "Spotted during the 0.4 release review." \ + --text-file /tmp/thread-open.md + +trellis channel post design-feedback comment \ + --scope global \ + --as reviewer \ + --thread login-empty-state \ + --text-file /tmp/review.md + +trellis channel post design-feedback status \ + --scope global \ + --as main \ + --thread login-empty-state \ + --status closed + +trellis channel post design-feedback summary \ + --scope global \ + --as main \ + --thread login-empty-state \ + --summary "Adopted the option-B layout; ticket TRELLIS-123 owns the fix." +``` + +Key distinctions: + +- `--description` is the **durable** thread description (the answer to "what + is this thread about?"). It is set on `opened` and edited by re-running + `post` with `--description`. +- `--text` / `--stdin` / `--text-file` is the **event body** — the comment or + payload attached to this specific timeline entry. +- `--labels` and `--assignees` are CSV and **replace** the current value; they + do not append. +- `--summary` is the rolling thread summary. Setting it on `status closed` is + the standard way to mark a thread resolved with context. + +`--thread` is required for every action except `opened` (where it is also +required in practice — there is no anonymous thread). + +## Read A Forum + +```bash +trellis channel messages design-feedback --scope global +trellis channel forum design-feedback --scope global --status open +trellis channel thread design-feedback login-empty-state --scope global +trellis channel messages design-feedback --scope global --raw --thread login-empty-state +``` + +If a peer says "I commented on the forum", run `channel forum` first to see +which thread changed, then drill into that thread with `channel thread <name> +<thread>`. Do not jump straight to ad-hoc `events.jsonl` parsing. + +## Context + +Context entries are durable background that should always be in scope when +reading a channel or a thread. They are **not** timeline events; they are +projected separately and replayed for every reader. + +Use the `context` subcommands. The legacy `--linked-context-file` / +`--linked-context-raw` flags on `create` and `post` are deprecated aliases +that fold into the canonical `--context-file` / `--context-raw`. + +### Add Context + +```bash +# Channel-level context (whole forum) +trellis channel context add design-feedback \ + --scope global \ + --raw "Upstream feedback board; please link tasks before opening threads." + +# Thread-level context (one thread) +trellis channel context add design-feedback \ + --scope global \ + --thread login-empty-state \ + --file "$PWD/.trellis/tasks/05-13-login-redesign/design.md" +``` + +- `--thread <key>` switches between channel-level and thread-level context. +- `--file` paths **must be absolute**; relative paths are rejected. +- `--raw` is plain text inline content. +- Both flags are repeatable; at least one is required for `add` / `delete`. +- `--as <agent>` records authorship; defaults to `main`. + +### List Context + +```bash +trellis channel context list design-feedback --scope global +trellis channel context list design-feedback --scope global --thread login-empty-state --raw +``` + +`--raw` on `list` emits one JSON entry per line (useful for piping); without +it you get a human-readable `file <path>` / `raw <truncated text>` listing. +An empty store prints `(no context)`. + +### Delete Context + +```bash +trellis channel context delete design-feedback \ + --scope global \ + --thread login-empty-state \ + --raw "stale note" +``` + +You delete by **value**, not by id: pass the same `--file` or `--raw` value +that was added. Repeat the flag to delete multiple entries in one call. + +### Reading Order + +When reading a thread, work top-down: + +1. Thread `description` (the durable "what is this about"). +2. Context entries (channel-level + thread-level). +3. Timeline (`opened`, `comment`, `status`, `summary`). + +If a context file is missing or unreadable, state that explicitly and +continue with the remaining data — do not fabricate the content. + +## Title Projection + +`title` projects a stable display title onto the channel without renaming the +storage address. The channel `name` you pass to every command stays the same. + +```bash +trellis channel title set design-feedback \ + --scope global \ + --title "Design feedback board" + +trellis channel title clear design-feedback --scope global +``` + +- `title set` requires `--title`. +- `--as <agent>` records authorship; defaults to `main`. +- This is a presentation-layer change. Tooling and scripts keep using the + original channel name. + +## Thread Rename + +`thread rename` is the correction path when a thread was opened with the +wrong key (typo, wrong slug convention, etc.). Threads do not support hard +deletion — rename is the supported corrective action. + +```bash +trellis channel thread rename design-feedback old-key new-key \ + --scope global \ + --as main +``` + +- `--as <agent>` is **required**. +- `post <name> rename` is rejected — you must use `thread rename`. + +## Deletion Discipline + +Do not model single-comment deletion or hard thread deletion as normal +workflow. Forum threads are append-only collaboration history. To correct +state, use: + +- `post ... status` to mark a thread closed / blocked / etc. +- `post ... summary` to record the resolution. +- `post ... --labels` to re-label (replaces the set). +- `thread rename` to correct a bad thread key. + +## Internal Changelog Pattern + +A common use of a global forum channel is an internal release / runtime +changelog. One thread per notable change keeps history searchable: + +```bash +trellis channel create release-notes \ + --type forum \ + --scope global \ + --description "Internal release and runtime changelog." \ + --context-raw "One thread per notable change; close when shipped." \ + --by main + +trellis channel post release-notes opened \ + --scope global \ + --as main \ + --thread release-2026-q1 \ + --title "Channel threads and forum UX in 0.6" \ + --description "Forum channel UX shipped in the 0.6 line." \ + --labels channel,release \ + --text-file /tmp/release-notes.md +``` + +Use stable, descriptive thread keys (e.g. `release-2026-q1`, +`runtime-event-schema-change`) so later readers can find them by name. diff --git a/.agents/skills/trellis-channel/references/progress-debugging.md b/.agents/skills/trellis-channel/references/progress-debugging.md new file mode 100644 index 00000000..3ed40d61 --- /dev/null +++ b/.agents/skills/trellis-channel/references/progress-debugging.md @@ -0,0 +1,226 @@ +# Progress And Debugging + +Pretty output is for operators. Raw output is the audit log. Subcommands +(`forum`, `thread`, `messages`, `context`) are the audit *interface* — reach +for them before grepping `events.jsonl` by hand. + +## Pretty vs `--raw` + +`trellis channel messages <channel>` renders a compact, human-readable view: +timestamps, identities, kind, and a short body. It is meant for operators +scanning a channel, not for diagnostics. + +Pretty output can and will truncate: + +- long progress deltas (`text_delta`, partial tool args) +- tool names and command lines +- multi-line status fields and structured `detail` blobs +- forum thread titles past the column budget + +When something looks "off" — a worker appears stuck, a progress line ends +mid-word, an action field shows `...` — switch to `--raw`. Raw mode emits +one JSON event per line exactly as it lives in `events.jsonl`, so nothing +is dropped. + +```bash +# Pretty (operator view) +trellis channel messages <channel> --kind done --last 10 +trellis channel messages <channel> --kind error --last 10 + +# Raw (diagnostic view) — one JSON per line +trellis channel messages <channel> --raw --kind progress --last 20 +trellis channel messages <channel> --raw --last 50 +``` + +Rule of thumb: never diagnose a worker from a truncated progress line. + +### Rebuild Streaming Text + +To reconstruct what a model actually streamed during a turn, concatenate +`detail.text_delta` from progress events: + +```bash +trellis channel messages <channel> --raw --kind progress --last 80 \ + | python3 -c 'import json,sys; [print((json.loads(l).get("detail") or {}).get("text_delta",""), end="") for l in sys.stdin if l.strip()]' +``` + +## Stalled Worker Diagnosis + +Symptom: `trellis channel list` shows the worker as running, but no new +events appear in `messages` and `wait` keeps timing out. + +Triage order: + +1. **Locate the channel files.** Use `list --all --all-projects` if you are + not sure which bucket the channel lives in. + + ```bash + trellis channel list --all --all-projects + CHAN=~/.trellis/channels/<bucket>/<channel> + ``` + +2. **Confirm the supervisor and worker PIDs are alive.** + + ```bash + cat "$CHAN/<worker>.pid" # supervisor PID + cat "$CHAN/<worker>.worker-pid" # actual CLI subprocess PID + ps -p "$(cat "$CHAN/<worker>.pid")" + ps -p "$(cat "$CHAN/<worker>.worker-pid")" + ``` + + If the supervisor PID is gone but the channel still lists the worker, + you have a ghost entry — clean it with + `trellis channel kill <name> --as <worker> --force`. + +3. **Tail the worker log.** This is the canonical place to see provider / + MCP / tool startup output that never makes it onto the channel. + + ```bash + tail -f "$CHAN/<worker>.log" + ``` + +4. **Check the last raw events.** A worker that emitted `progress` but no + `message`/`done` is usually mid-stream or blocked on a tool call: + + ```bash + trellis channel messages <channel> --raw --last 50 + ``` + +Common "alive but silent" causes: + +- Provider cold start before the first token (long, but eventually moves). +- A blocking MCP server during startup — visible in the worker log. +- Worker is waiting for a tool result whose subprocess hung. +- Prompt is huge / model is rate-limited; check provider-side errors in the + worker log. + +## Progress Event Interpretation + +A `progress` event represents an in-flight piece of work. Its shape varies +by `action` field, but the load-bearing fields are always under `detail`: + +- `detail.text_delta` — incremental model output (concatenate across events + to rebuild the streamed reply). +- `detail.tool_name`, `detail.tool_input` — tool call about to run or + currently running. +- `detail.status` — short string used by long-running actions + (`starting`, `running`, `flushing`, `done`). +- `detail.action` — semantic label (e.g. `status` for thread heartbeats). + +Progress events are **noisy** by design. `wait` ignores them unless you +pass `--include-progress`. When you do want to see them, prefer: + +```bash +trellis channel messages <channel> --raw --kind progress --last 80 +``` + +A stream that emits progress at a steady cadence but never closes with +`done`/`error`/`message` is the classic shape of a hung tool call — +inspect the worker log for the subprocess. + +## Wait Semantics (Quick Reference) + +`channel wait` watches `events.jsonl` from EOF and wakes on: + +- `message` +- `done` +- `error` +- `killed` +- `progress` only with `--include-progress` + +Useful filters: + +```bash +trellis channel wait T --as main --from check --kind done --timeout 15m +trellis channel wait T --as main --from check,check-cx --kind done --all --timeout 15m +trellis channel wait T --as worker --tag interrupt --timeout 1h +trellis channel wait T --as main --thread release-note --action status --timeout 10m +``` + +Exit codes: `0` matched, `124` timeout, `1`/`2` errors. On `wait --all` +timeout, stderr names the workers still missing. + +## Auditing `events.jsonl` — Use Subcommands, Not `grep` + +Every channel persists its full history at `$CHAN/events.jsonl`. It is +tempting to `tail` / `grep` / `jq` this file directly during debugging. +Don't make it a habit, and **never** do it for forum channels. + +Why subcommands first: + +- `messages` already replays the file with filters (`--kind`, `--from`, + `--last`, `--tag`, `--thread`, `--action`) and gives you `--raw` for the + exact JSON. Anything you would write a one-liner for, `messages` already + does. +- `wait` consumes the same file with EOF semantics — re-implementing that + with `tail -f | jq` will drop events under load and misorder them under + rotation. +- `context` materializes a worker's inbox view, including cursor state. + Hand-rolled filters do not respect `<worker>.inbox-cursor`. + +### Forum channels: never parse `events.jsonl` directly + +Forum channels multiplex many logical threads onto a single `events.jsonl`. +Each event carries `thread`, `action`, and tag fields that the forum +subcommands know how to fold together. Parsing the file by hand will: + +- Mix threads together and make a thread look incoherent. +- Miss thread lifecycle events (open / status / close) that change how + later events should be interpreted. +- Ignore worker inbox cursors, so you will "see" events a worker has + already consumed and assume they are pending. + +Use the forum-aware views instead: + +```bash +# List logical threads inside the forum channel +trellis channel forum list <channel> + +# Inspect one thread end-to-end +trellis channel thread show <channel> <thread> + +# Replay messages for a thread (supports --raw, --kind, --last) +trellis channel messages <channel> --thread <thread> --raw --last 100 + +# What a specific worker still has pending +trellis channel context <channel> --as <worker> +``` + +Direct reads of `events.jsonl` are reserved for the case where the CLI +itself is suspect — e.g. confirming an event was actually persisted, or +diffing against `<worker>.inbox-cursor` while debugging the supervisor. + +## Common Failures + +| Symptom | Cause | Fix | +|---|---|---| +| `trellis: command not found` | CLI not installed globally | `npm install -g @mindfoldhq/trellis` | +| `wait` exits immediately | wrong filter or identity collision | use distinct `--as`, inspect raw messages | +| zsh errors on message text | shell interpreted punctuation | use `--stdin` or `--text-file` | +| progress line is cut off | pretty output truncation | use `messages --raw --kind progress` | +| worker never speaks | provider startup / prompt / MCP delay | inspect `<worker>.log`, `ps`, raw events | +| channel not found in another cwd | project bucket mismatch | `cd` to project, use `--scope global`, or `list --all-projects` | +| ghost worker in list | supervisor died without cleanup | `trellis channel kill <name> --as <worker> --force` | +| forum thread looks scrambled | parsed `events.jsonl` directly | use `forum`, `thread`, `messages --thread` | + +## Storage Layout + +```text +~/.trellis/channels/ +└── <bucket>/ + └── <channel-name>/ + ├── events.jsonl + ├── <channel>.lock + ├── <worker>.log + ├── <worker>.pid + ├── <worker>.worker-pid + ├── <worker>.config + ├── <worker>.session-id + ├── <worker>.thread-id + ├── <worker>.inbox-cursor + └── <worker>.spawnlock +``` + +Agents normally use the CLI, not direct file reads. Direct file reads are +for debugging when CLI views are insufficient — and even then, never on a +forum channel's `events.jsonl`. diff --git a/.agents/skills/trellis-channel/references/workers.md b/.agents/skills/trellis-channel/references/workers.md new file mode 100644 index 00000000..bcec98fc --- /dev/null +++ b/.agents/skills/trellis-channel/references/workers.md @@ -0,0 +1,276 @@ +# Workers And Agent Cards + +Use workers when a peer agent should execute independently and report back +through the channel event log. A worker is a registered child process (claude +or codex) attached to a channel; the supervisor forwards inbox messages to it +and translates its output back into channel events. + +## Spawn + +```bash +trellis channel create impl-task --by dispatcher --cwd /path/to/repo +trellis channel spawn impl-task --provider codex --as codex-impl --timeout 30m + +echo "Implement the schema for table X per .trellis/.../prd.md" \ + | trellis channel send impl-task --as dispatcher --to codex-impl --stdin + +trellis channel wait impl-task --as dispatcher --from codex-impl --kind done --timeout 30m +``` + +`spawn` forks a `channel __supervisor` worker that emits `spawned`, streams +`progress`, and should end with `done`, `error`, or `killed`. Workers stay +inbox-idle until a `send --to <worker>` (or a broadcast when +`--inbox-policy broadcastAndExplicit` is set) wakes them. + +Key `spawn` flags: + +- `--agent <name>` — load `.trellis/agents/<name>.md` (provider/model/as/system prompt defaults). +- `--provider <claude|codex>` — overrides the agent card; validated against the adapter registry. +- `--as <name>` — channel worker handle; defaults to the agent name. +- `--cwd <path>` — worker working directory (also the jail root for `--file`/`--jsonl`). +- `--model <id>` — model override. +- `--resume <id>` — resume an existing claude session / codex thread. +- `--timeout <duration>` — auto-kill after `30s` / `2m` / `1h`. +- `--warn-before <duration>` — supervisor_warning lead time (default `5m`; `0ms` disables). +- `--file <path>` (repeatable, glob-supported) — inject file content into the system prompt. +- `--jsonl <path>` (repeatable) — Trellis jsonl manifest (`{file, reason}` per line). +- `--by <agent>` — author of the `spawned` event (defaults to `$TRELLIS_CHANNEL_AS` or `main`). +- `--inbox-policy <explicitOnly|broadcastAndExplicit>` — default `explicitOnly`. +- `--idle-timeout <duration>` — OOM guard idle TTL (default `5m`; `0` disables). +- `--max-live-workers <n>` — spawn-time live-worker budget (default `6`; `0` disables). + +The success event `spawned` records `pid`, `provider`, `agent`, the injected +`files`, and the resolved `manifests` so later spectators can audit context. + +## Agent Cards + +`--agent <name>` resolves to `.trellis/agents/<name>.md`. The card name must +match `[A-Za-z0-9._-]+`. The default Trellis install ships two cards: + +- `.trellis/agents/check.md` — code-quality reviewer. +- `.trellis/agents/implement.md` — coding worker for implementation runs. + +```yaml +--- +name: check +description: Code quality check expert. +provider: claude +--- +``` + +Frontmatter fields populate `spawn` defaults (provider, model, `as`); the +markdown body becomes the worker's system-prompt role. Cards do **not** +auto-attach task files — context must be injected explicitly per spawn (see +below). + +Always inspect project cards before spawning a named agent: + +```bash +ls .trellis/agents +sed -n '1,100p' .trellis/agents/check.md +``` + +## Context Injection + +Two flags inject content into the worker's system prompt under a +`# CONTEXT FILES` block, assembled by `context-loader`: + +- `--file <path>` — repeatable, glob-supported (`*`, `**`). Each match is + read and concatenated. +- `--jsonl <path>` — repeatable Trellis manifest where every line is + `{"file":"<path>","reason":"<why>"}`. The reason is preserved as a header + comment above each file's content. + +Limits enforced by the loader: + +- 1 MB hard cap per file (oversize → error). +- 200 KB per-file warning to stderr. +- 500 KB total assembled-context warning to stderr. +- Path-traversal jail: all resolved paths must stay under `--cwd`. + +Example spawning a check agent against a task directory: + +```bash +TASK=.trellis/tasks/05-13-example +trellis channel spawn cr-example --agent check --provider codex --as check-cx \ + --file "$TASK/prd.md" \ + --file "$TASK/design.md" \ + --file "$TASK/implement.md" \ + --jsonl "$TASK/check.jsonl" \ + --cwd "$PWD" --timeout 30m +``` + +The `spawned` event records both the literal `files` array and any `manifests` +expanded from `--jsonl`, so the audit trail captures whatever the worker was +actually shown. + +## Names And Routing + +`--as` has two meanings: + +- `send` / `wait` / `interrupt`: speaker identity (author of the resulting event). +- `spawn`: the worker handle that other agents address with `--to`. + +Use explicit names when multiple workers or providers participate in one +channel: + +```bash +trellis channel spawn cr-feature --agent check --as check-claude +trellis channel spawn cr-feature --agent check --provider codex --as check-cx + +trellis channel wait cr-feature --as main \ + --from check-claude,check-cx --kind done --all --timeout 15m +``` + +`--all` requires `--from` and blocks until every listed worker has produced a +matching event; timeout exits with code **124** and prints +`timeout: still waiting on ...` to stderr. + +## Soft Interrupt — `interrupt` + +`channel interrupt` is the cooperative redirect: it appends an `interrupt` +event (reason `"user"`) and, where the adapter supports it, issues a +provider-level turn interrupt with a replacement instruction. Use it when the +worker should drop its current turn and act on new input immediately, without +losing its session. + +```bash +echo "Stop refactoring the parser — switch to fixing the failing test in src/foo.ts" \ + | trellis channel interrupt impl-task --as dispatcher --to codex-impl --stdin +``` + +Flags: + +- `--as <agent>` **(required)** — caller identity. +- `--to <agent>` **(required)** — target worker. +- `--scope <project|global>` — channel scope. +- `--stdin` / `--text-file <path>` / `[text]` — replacement instruction body. + +The appended event has `kind: "interrupt"` — downstream `wait` / `messages` +filters can subscribe with `--kind interrupt` to react to redirections (e.g. +to log the rerouting, or to gate other workers behind a coordinator's +correction). + +For low-priority hints that should wait for the worker's next turn, send a +plain tagged message instead: + +```bash +echo "Check this when you reach the next turn." \ + | trellis channel send impl-task --as dispatcher --to codex-impl \ + --stdin --tag question +``` + +## Hard Interrupt — `kill` + `--resume` + +Use `kill` when the worker must stop **now** (e.g. runaway loop, bad +instructions already in flight, or `interrupt` is not honored by the +adapter). The supervisor escalates SIGTERM → 8 s grace → SIGKILL; the CLI +writes a `killed` event when SIGKILL is needed so the event log stays +truthful. + +```bash +trellis channel kill impl-task --as codex-impl +trellis channel spawn impl-task --as codex-impl --provider codex \ + --resume "$(cat ~/.trellis/channels/<bucket>/impl-task/worker.session-id)" + +echo "STOP — new instructions: ..." \ + | trellis channel send impl-task --as dispatcher --to codex-impl --stdin +``` + +`kill` flags: + +- `--as <agent>` **(required)** — names the worker (positional `<name>` is the channel). +- `--scope <project|global>`. +- `--force` — SIGKILL immediately (also kills the inner worker pid). + +Side effects: cleans `pid`, `worker-pid`, `config`, `spawnlock` sidecar +files; keeps `log`, `session-id`, `thread-id` for forensics and resume. + +When `interrupt` will not converge, kill + `--resume` is the guaranteed +redirection path. + +## Worker OOM Guard + +The OOM guard prevents orphaned/idle workers from accumulating and exhausting +host resources. It runs at every `spawn` and enforces two policies per +project bucket: + +- **Idle TTL** — sweep workers whose last activity is older than the + configured threshold (default `5m`; `0` disables). +- **Live-worker budget** — refuse the new spawn if more than N workers are + already alive in the same project bucket (default `6`; `0` disables). + +Precedence (highest first): + +1. CLI flags: `--idle-timeout`, `--max-live-workers` on `spawn`. +2. Environment variables: `TRELLIS_CHANNEL_WORKER_IDLE_TIMEOUT`, + `TRELLIS_CHANNEL_MAX_LIVE_WORKERS`. +3. `.trellis/config.yaml` under `channel.worker_guard`. +4. Built-in defaults (`5m`, `6`). + +Cleanup notices are written to stderr at spawn time so operators can see which +idle workers were swept and why a new spawn was rejected. The guard does not +touch ephemeral / `channel run` workers any differently — they are subject to +the same idle TTL and budget. + +To audit current state, list workers via `channel list` (the `WORKERS` +column) and inspect per-channel `pid` / `worker-pid` sidecar files under +`~/.trellis/channels/<bucket>/<channel>/`. + +## Worker Inbox APIs + +The inbox is the channel surface workers wake on. Routing is controlled by +two knobs: + +- **Inbox policy** (`spawn --inbox-policy`): + - `explicitOnly` (default) — worker only wakes on `send --to <worker>` or + `interrupt --to <worker>`. + - `broadcastAndExplicit` — also wakes on broadcasts (`send` with no `--to`). +- **Delivery mode** (`send --delivery-mode`): + - `appendOnly` — append the event regardless of worker state. + - `requireKnownWorker` — fail if no worker named in `--to` was ever spawned. + - `requireRunningWorker` — fail if the named worker is not currently alive. + +Stricter delivery modes prevent silent message loss when callers expect a +running peer. + +Inbox-relevant subcommands: + +- `send <channel> [text]` — append a `message` event. + - `--as <agent>` **(required)** — author. + - `--to <agents>` — CSV; one → string, many → array; broadcast if omitted. + - `--stdin` / `--text-file <path>` / `[text]` — body source. + - `--delivery-mode <appendOnly|requireKnownWorker|requireRunningWorker>`. +- `interrupt <channel> [text]` — soft-interrupt redirect (see above). +- `wait <channel>` — block until matching events arrive. + - `--as <agent>` **(required)** — `self` for filter context. + - `--from <agents>` — CSV authors. + - `--kind <kind[,kind...]>` — CSV (OR semantics); supports `interrupt`, + `done`, `progress`, etc. + - `--to <target>` — defaults to own agent (broadcast + explicit-to-me). + - `--include-progress` — also wake on progress events. + - `--all` — require every `--from` agent to match (timeout → exit **124**). + - `--timeout <duration>` — `30s` / `2m` / `1h` / `1000ms`. +- `messages <channel>` — view / filter / follow the event stream. + - `--follow` to tail, `--kind` / `--from` / `--to` to filter, `--raw` for + JSON-per-line, `--no-progress` to hide progress noise. + +A typical dispatcher loop: + +```bash +# 1. Wake the worker. +echo "Run the failing test and report." \ + | trellis channel send impl-task --as dispatcher --to codex-impl --stdin \ + --delivery-mode requireRunningWorker + +# 2. Block until it finishes. +trellis channel wait impl-task --as dispatcher \ + --from codex-impl --kind done,error --timeout 30m + +# 3. Read the final answer. +trellis channel messages impl-task --from codex-impl --last 1 --raw +``` + +All event-emitting subcommands (`send`, `interrupt`, `post`, `context add` / +`delete`, `title set` / `clear`, `thread rename`) print the appended event as +a single JSON line on stdout, making the inbox layer easy to script against. diff --git a/.agents/skills/trellis-channel/references/workflows.md b/.agents/skills/trellis-channel/references/workflows.md new file mode 100644 index 00000000..33197643 --- /dev/null +++ b/.agents/skills/trellis-channel/references/workflows.md @@ -0,0 +1,128 @@ +# Workflows + +Use these patterns by intent. Prefer durable channels for multi-round work and +`channel run` for one-shot questions. + +## Pattern A: Multi-round Brainstorm + +Use when the user says "和 codex/claude 讨论一下", "brainstorm", or "拉一个 agent +进来一起看". + +```bash +trellis channel create brainstorm-storage-layer --by main \ + --task .trellis/tasks/05-XX-storage-adapter + +trellis channel spawn brainstorm-storage-layer \ + --agent architect --provider codex \ + --file .trellis/tasks/05-XX-storage-adapter/prd.md \ + --file .trellis/tasks/05-XX-storage-adapter/design.md \ + --as cx-arch --timeout 30m + +trellis channel send brainstorm-storage-layer \ + --as main --to cx-arch --text-file /tmp/brainstorm-r1.md + +trellis channel wait brainstorm-storage-layer \ + --as main --kind done --from cx-arch --timeout 10m +``` + +Do not stop after one answer. Read the answer, identify vague areas, send a +new probe, and repeat until the result is executable. + +Minimum round structure: + +1. Direction split: should this live in an existing mechanism or a new one? +2. MVP boundary: v1, v2, and what would force v2 back into v1. +3. Data contract: events, schema, metadata, state source of truth, compatibility. +4. CLI / UX contract: command names, flags, errors, defaults, ambiguity. +5. Cross-layer risk and tests: shared helpers, drift points, release-blocking tests. + +Optional rounds: + +- Operations: logs, debugging, stuck workers, kill/restart, recovery. +- Migration/release: breaking status, manifest, changelog, docs-site. +- Opposition review: ask the peer agent to argue against the current plan. + +Every probe should request concrete file paths, commands, schema, rejected +alternatives, and release-blocking issues. Reject hedging when a decision is +needed. + +## Pattern B: Implement / Check Agent + +Use when the user asks to dispatch implementation or review work. + +```bash +TASK=.trellis/tasks/05-12-foo +trellis channel create cr-foo --task "$TASK" --by main + +trellis channel spawn cr-foo \ + --agent check \ + --jsonl "$TASK/check.jsonl" \ + --file "$TASK/prd.md" \ + --file "$TASK/design.md" \ + --file "$TASK/implement.md" \ + --cwd "$PWD" --timeout 15m + +trellis channel send cr-foo --as main --to check --text-file /tmp/cr-brief.md +trellis channel wait cr-foo --as main --kind done --from check --timeout 15m +trellis channel messages cr-foo --kind message --from check --tag final_answer +``` + +For implement work, use `--agent implement` and send an implementation brief. +For check work, include the exact diff scope, relevant specs, and validation +already run. + +## Pattern C: Parallel Reviewers + +Use one channel and distinct worker names. + +```bash +trellis channel create cr-feature --by main --ephemeral + +trellis channel spawn cr-feature --agent check \ + --jsonl "$TASK/check.jsonl" --file "$TASK/prd.md" --file "$TASK/design.md" \ + --timeout 15m + +trellis channel spawn cr-feature --agent check --provider codex --as check-cx \ + --jsonl "$TASK/check.jsonl" --file "$TASK/prd.md" --file "$TASK/design.md" \ + --timeout 15m + +trellis channel send cr-feature --as main --to check --text-file /tmp/cr-brief.md +trellis channel send cr-feature --as main --to check-cx --text-file /tmp/cr-brief.md +trellis channel wait cr-feature --as main --kind done --from check,check-cx --all --timeout 15m +``` + +`--all` means every listed worker must emit a matching event. + +## Pattern D: One-shot Worker + +```bash +trellis channel run --provider codex --message "say hi in 3 words" --timeout 1m +trellis channel run --agent plan --message-file /tmp/plan-question.md --timeout 10m +``` + +On success, `run` removes the ephemeral channel. On error/timeout/killed, it +keeps the channel and prints the path for inspection. + +## Pattern E: Forum Channel + +Use for issue forums, topic-style feedback, release todos, agent findings, and +internal changelogs. Read `forum.md` for the full model. + +## Pattern F: Take Over Existing Thread + +If the user gives a forum/thread name, restore context yourself: + +```bash +trellis channel forum <board> --scope global +trellis channel thread <board> <thread> --scope global --raw +trellis channel context list <board> --scope global --thread <thread> +trellis channel messages <board> --scope global --raw --thread <thread> +``` + +Output a constraint summary, not a transcript dump: + +- user-level problem +- context files that affect this repo +- current-version versus future-version requirements +- whether current code/design satisfies it +- next action or comment to append diff --git a/.agents/skills/trellis-check/SKILL.md b/.agents/skills/trellis-check/SKILL.md index 16b3dc49..c695abda 100644 --- a/.agents/skills/trellis-check/SKILL.md +++ b/.agents/skills/trellis-check/SKILL.md @@ -16,7 +16,13 @@ git diff --name-only HEAD git status ``` -## Step 2: Read Applicable Specs +## Step 2: Read Task Artifacts and Applicable Specs + +Read the current task artifacts in order: + +- `prd.md` +- `design.md` if present +- `implement.md` if present ```bash python3 ./.trellis/scripts/get_context.py --mode packages diff --git a/.agents/skills/trellis-continue/SKILL.md b/.agents/skills/trellis-continue/SKILL.md index 74dea8f8..4aceae79 100644 --- a/.agents/skills/trellis-continue/SKILL.md +++ b/.agents/skills/trellis-continue/SKILL.md @@ -27,20 +27,21 @@ Shows the Phase Index (Plan / Execute / Finish) with routing + skill mapping. ## Step 3: Decide Where You Are -`get_context.py` shows the active task's `status` field. Route by `status` + artifact presence: +`get_context.py` shows the active task's `status` field. Route by `status` + artifact presence. This command replaces the user needing to remember the Trellis flow; it does not itself approve implementation. - `status=planning` + no `prd.md` → **1.1** (load `trellis-brainstorm`) -- `status=planning` + `prd.md` exists + `implement.jsonl` not curated (only the seed `_example` row) → **1.3** -- `status=planning` + `prd.md` + curated `implement.jsonl` → **1.4** (run `task.py start` to enter Phase 2) +- `status=planning` + `prd.md` only → decide whether the task is lightweight or complex. Lightweight can move to **1.4** review; complex returns to **1.1** to add `design.md` + `implement.md`. +- `status=planning` + complex artifacts complete + sub-agent jsonl not curated (only the seed `_example` row) → **1.3** +- `status=planning` + required artifacts complete + required jsonl curated or inline mode → **1.4** (ask for start review; only run `task.py start` after user confirms) - `status=in_progress` + implementation not started → **2.1** - `status=in_progress` + implementation done, not yet checked → **2.2** -- `status=in_progress` + check passed → **3.1** +- `status=in_progress` + check passed → **3.3** (spec update) → **3.4** (commit) - `status=completed` (rare; usually archived immediately) → archive flow Phase rules (full detail in `.trellis/workflow.md`): 1. Run steps **in order** within a phase — `[required]` steps must not be skipped -2. `[once]` steps are already done if the output exists (e.g., `prd.md` for 1.1; `implement.jsonl` with curated entries for 1.3) — skip them +2. `[once]` steps are already done if the required output exists. `prd.md` alone can be enough only for lightweight tasks; complex tasks also need `design.md` and `implement.md`. 3. You may go back to an earlier phase if discoveries require it ## Step 4: Load the Specific Step @@ -57,4 +58,4 @@ Follow the loaded instructions. After each `[required]` step completes, move to ## Reference -Full workflow, skill routing table, and the DO-NOT-skip table live in `.trellis/workflow.md`. This command is only an entry point — the canonical guidance is there. +Full workflow and detailed phase steps live in `.trellis/workflow.md`. This command is only an entry point — the canonical guidance is there. diff --git a/.agents/skills/trellis-meta/SKILL.md b/.agents/skills/trellis-meta/SKILL.md index 590bfac3..0ffe5939 100644 --- a/.agents/skills/trellis-meta/SKILL.md +++ b/.agents/skills/trellis-meta/SKILL.md @@ -1,73 +1,85 @@ --- name: trellis-meta -description: "Understand and customize the local Trellis architecture inside a user project. Use when modifying .trellis plus platform hooks, settings, agents, skills, commands, prompts, or workflows generated by trellis init." +description: "Understand and customize the local Trellis architecture inside a user project. Use when modifying .trellis plus platform hooks, settings, agents, skills, commands, prompts, workflows, the channel runtime (trellis channel), bundled runtime agents under .trellis/agents/, selectable workflow templates, registry-backed spec refresh, cross-session memory (trellis mem) generated by trellis init, or AI-facing bundled skills (trellis-channel, trellis-session-insight, trellis-spec-bootstrap) and bundled-skill auto-dispatch flow." --- # Trellis Meta This skill is for local Trellis users who have already run `trellis init` in a project. After reading it, an AI should understand the Trellis architecture, operating model, and customization entry points inside that user project, then modify the generated `.trellis/` and platform directory files according to the user's request. +Trellis v0.6 adds three architectural surfaces on top of the pre-v0.6 workflow / persistence / platform model. First, a multi-agent collaboration runtime: `trellis channel` coordinates multiple AI worker processes through project-scoped JSONL event logs at `~/.trellis/channels/<project>/<channel>/events.jsonl`, with worker OOM guard, forum/thread channels, durable idempotency keys, and bundled `.trellis/agents/{check,implement}.md` runtime definitions. Second, cross-session memory: `trellis mem list | search | context | extract | projects` reads raw Claude Code, Codex, and Pi Agent JSONL already on disk, slices by `--phase brainstorm|implement|all`, and never uploads anything. Third, a dual-package npm release: `@mindfoldhq/trellis` (CLI) and `@mindfoldhq/trellis-core` (SDK with `/channel`, `/task`, `/mem`, `/testing` subpaths) ship in lockstep on one version. Treat these as first-class customization surfaces alongside the per-platform integration files. + The default operating scope is local files in the user project: -- `.trellis/`: workflow, config, tasks, spec, workspace, scripts, and runtime state. -- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories. +- `.trellis/`: workflow, config, tasks, spec, workspace, scripts, bundled runtime agents, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.reasonix/`, `.kilocode/`, `.agent/`, `.devin/`, `.kimi-code/`, and similar directories. Pi additionally exposes a native `trellis_subagent` tool with `single` / `parallel` / `chain` dispatch modes, throttled progress cards, and `isTrellisAgent()` validation on top of the file layout. Reasonix stores both workflow skills and subagent skills as `.reasonix/skills/<name>/SKILL.md`; subagent skills carry `runAs: subagent` frontmatter. Kimi Code keeps workflow skills in the shared `.agents/skills/` layer and delivers commands plus agent prompts as `.kimi-code/skills/<name>/SKILL.md`. - Shared skill layer: `.agents/skills/`. +- User-owned channel store outside the project tree: `~/.trellis/channels/<project>/<channel>/events.jsonl`. +- Raw platform conversation logs queryable via `trellis mem`: `~/.claude/projects/`, `~/.codex/sessions/`, and `~/.pi/agent/sessions/` (OpenCode adapter degraded for the v0.6 line). -Do not assume the user has the Trellis source repository. Do not default to modifying the global npm install directory or `node_modules`. +Do not assume the user has the Trellis source repository. Do not default to modifying the global npm install directory or `node_modules` — both `@mindfoldhq/trellis` and `@mindfoldhq/trellis-core` ship as published packages sharing one version and one git tag per release. ## How To Use 1. Read `references/local-architecture/overview.md` first to establish the local Trellis system model. 2. If the request involves a specific AI tool, read `references/platform-files/platform-map.md` and the relevant platform file notes. -3. If the user wants to change behavior, read `references/customize-local/overview.md`, then open the specific customization topic. -4. Before editing, read the actual files in the user project and treat local content as authoritative. +3. If the request involves multi-agent dispatch or channel workers, read `references/local-architecture/multi-agent-channel.md` and the bundled `.trellis/agents/` files. +4. If the user wants to change behavior, read `references/customize-local/overview.md`, then open the specific customization topic. +5. Before editing, read the actual files in the user project and treat local content as authoritative. ## References ### Local Architecture -- `references/local-architecture/overview.md`: The three-layer local Trellis architecture and customization principles. -- `references/local-architecture/generated-files.md`: Files generated by `trellis init` and their customization boundaries. -- `references/local-architecture/workflow.md`: Phases, routing, and workflow-state blocks in `.trellis/workflow.md`. -- `references/local-architecture/task-system.md`: Task directories, active tasks, JSONL context, and task runtime. -- `references/local-architecture/spec-system.md`: How `.trellis/spec/` is organized and injected. -- `references/local-architecture/workspace-memory.md`: `.trellis/workspace/`, journals, and cross-session memory. -- `references/local-architecture/context-injection.md`: Hooks, sub-agent preludes, and context injection paths. +- `references/local-architecture/overview.md`: The layered local Trellis architecture (workflow / persistence / platform / channel runtime) and customization principles. +- `references/local-architecture/generated-files.md`: Files generated by `trellis init` and their customization boundaries, including `.trellis/agents/`. +- `references/local-architecture/workflow.md`: Phases, routing, workflow-state blocks, and selectable workflow templates (`native`, `tdd`, `channel-driven-subagent-dispatch`, marketplace) in `.trellis/workflow.md`. +- `references/local-architecture/task-system.md`: Task directories, active task, JSONL context, parent/child task trees, and task runtime. +- `references/local-architecture/spec-system.md`: How `.trellis/spec/` is organized, injected, and refreshed from a `registry.spec` source. +- `references/local-architecture/workspace-memory.md`: `.trellis/workspace/` journals plus `trellis mem` cross-session recall and the `@mindfoldhq/trellis-core/mem` SDK. +- `references/local-architecture/context-injection.md`: Hooks, sub-agent preludes, and channel-runtime worker inbox routing. +- `references/local-architecture/multi-agent-channel.md`: `trellis channel` subcommands, project-scoped event store, forum/thread channels, worker OOM guard, durable idempotency, and bundled `.trellis/agents/` runtime agents. +- `references/local-architecture/bundled-skills.md`: Auto-dispatched bundled skills (`trellis-meta`, `trellis-spec-bootstrap`, `trellis-session-insight`) and how `getBundledSkillTemplates()` ships them to every platform skill root. ### Platform Files -- `references/platform-files/overview.md`: How shared `.trellis/` files relate to platform directories. -- `references/platform-files/platform-map.md`: Platform directories and paths for skills, agents, hooks, and extensions. -- `references/platform-files/hooks-and-settings.md`: How settings/config files, hooks, plugins, and extensions connect to Trellis. -- `references/platform-files/agents.md`: Local file responsibilities for `trellis-research`, `trellis-implement`, and `trellis-check`. +- `references/platform-files/overview.md`: How shared `.trellis/` files relate to platform directories and the four platform integration modes (hook-driven, agent prelude, main-session workflow, channel runtime). +- `references/platform-files/platform-map.md`: Platform directories and paths for skills, agents, hooks, and extensions across all supported platforms including Reasonix and Pi's native `trellis_subagent` extension. +- `references/platform-files/hooks-and-settings.md`: How settings/config files, hooks, plugins, and extensions connect to Trellis; covers `channel.worker_guard.*` and `codex.dispatch_mode`. +- `references/platform-files/agents.md`: Per-platform `trellis-research` / `trellis-implement` / `trellis-check` sub-agent files plus bundled `.trellis/agents/{check,implement}.md` for the channel runtime. - `references/platform-files/skills-and-commands.md`: Differences between skills, commands, prompts, and workflows, plus how to change them. ### Local Customization - `references/customize-local/overview.md`: Choose the right local customization entry point for the user's request. -- `references/customize-local/change-workflow.md`: Change phases, routing, next actions, and workflow-state. -- `references/customize-local/change-task-lifecycle.md`: Change task creation, status, archive behavior, and hooks. -- `references/customize-local/change-context-loading.md`: Change how tasks, specs, journals, and hook context are loaded. -- `references/customize-local/change-hooks.md`: Change platform hooks, settings, and shell session bridges. -- `references/customize-local/change-agents.md`: Change research, implement, and check agent behavior. -- `references/customize-local/change-skills-or-commands.md`: Add or modify local skills, commands, prompts, and workflows. -- `references/customize-local/change-spec-structure.md`: Adjust the project spec structure under `.trellis/spec/`. +- `references/customize-local/change-workflow.md`: Change phases, routing, next actions, workflow-state, and the selected workflow template. +- `references/customize-local/change-task-lifecycle.md`: Change task creation, status, archive behavior, parent/child links, archive slug collision handling, and lifecycle hooks. +- `references/customize-local/change-context-loading.md`: Change how tasks, specs, journals, hook context, channel inbox messages, and `trellis mem` recall are loaded. +- `references/customize-local/change-hooks.md`: Change platform hooks, settings, task lifecycle hooks (`hooks.after_*`), and shell session bridges. +- `references/customize-local/change-agents.md`: Change research, implement, and check agent behavior across platform sub-agents, bundled channel runtime agents, and the Codex `dispatch_mode` toggle. +- `references/customize-local/change-skills-or-commands.md`: Add or modify local skills, commands, prompts, and workflows; covers upstream bundled-skill auto-dispatch. +- `references/customize-local/change-spec-structure.md`: Adjust the project spec structure under `.trellis/spec/`, including registry-backed sources. - `references/customize-local/add-project-local-conventions.md`: Put team rules into project-local specs or local skills. ## Current Rules -- `.trellis/workflow.md` is the local workflow source of truth. -- `.trellis/config.yaml` is the project-level Trellis configuration and task hook configuration entry point. -- `.trellis/spec/` stores the user's project-specific coding conventions and design constraints. -- `.trellis/tasks/` stores task PRDs, technical notes, research files, and JSONL context. -- `.trellis/workspace/` stores developer journals and cross-session memory. -- Platform settings/config files decide which hooks, agents, skills, commands, prompts, and workflows actually run. +- `.trellis/workflow.md` is the local workflow source of truth; its initial content was selected from a workflow template (built-in `native`, `tdd`, `channel-driven-subagent-dispatch`, or a marketplace template) at `trellis init` time and can be re-selected via `trellis workflow --template <id>`. Missing `.trellis/agents/<name>.md` files referenced by the active template trigger a non-blocking stderr warning pointing at `trellis update`. +- `.trellis/config.yaml` is the project-level Trellis configuration entry point. It hosts task lifecycle hooks (`hooks.after_create` / `after_start` / `after_finish` / `after_archive`), journal shape (`session_commit_message` / `max_journal_lines` / `session_auto_commit`), channel worker guard (`channel.worker_guard.idle_timeout` / `max_live_workers`), Codex dispatch mode (`codex.dispatch_mode: inline | sub-agent`), and the spec registry block (`registry.spec.source` + `registry.spec.template`). +- `.trellis/spec/` stores the user's project-specific coding conventions and design constraints. When `registry.spec` is set, files are refreshed by `trellis update`; local edits surface as "modified by user" conflicts in `.trellis/.template-hashes.json`. +- `.trellis/tasks/` stores task PRDs, design notes, implement plans, research files, and JSONL context. Tasks form parent/child trees: `task.py create --parent <slug>`, `task.py add-subtask <parent> <child>`, `task.py remove-subtask <parent> <child>`, and `task.py list-context <task>`. `task.py create` rejects a slug already present in `.trellis/tasks/archive/**`. +- `.trellis/workspace/` stores **deliberately written** developer journals. Raw cross-session dialogue is **not** stored here — it lives on disk under `~/.claude/projects/`, `~/.codex/sessions/`, and `~/.pi/agent/sessions/` and is recovered via `trellis mem search|extract|context`. The bundled `trellis-session-insight` skill teaches when to reach for `mem`. +- `.trellis/agents/{check,implement}.md` are bundled, platform-agnostic channel runtime agent definitions loaded by `trellis channel spawn --agent <name>`. Editable; `trellis update` backfills missing ones. Editing the per-platform `trellis-implement.md` / `trellis-check.md` does **not** change channel-runtime worker behavior. +- `~/.trellis/channels/<project>/<channel>/events.jsonl` is the channel runtime event log per project per channel. User-owned, file-locked sequence numbering, durable `idempotencyKey` support; never under `.trellis/`. +- Bundled multi-file skills (`trellis-meta`, `trellis-spec-bootstrap`, `trellis-session-insight`, `trellis-channel`) are auto-dispatched to every platform skill root by `getBundledSkillTemplates()` in `packages/cli/src/templates/common/index.ts`. Dropping a new directory under `packages/cli/src/templates/common/bundled-skills/` (upstream) ships it to every platform on the next `trellis update`. +- Platform settings/config files decide which hooks, agents, skills, commands, prompts, and workflows actually run. Reasonix has no settings file — behavior is encoded inside skill frontmatter. - `.trellis/.template-hashes.json` and `.trellis/.runtime/` are management/runtime state files. Confirm necessity before editing them. ## Do Not - Do not treat Trellis upstream source code as the default target for local customization. -- Do not modify the global npm install directory or `node_modules/@mindfoldhq/trellis` to implement project needs. -- Do not overwrite user-modified local files with default templates. -- Do not put team-private project rules into the public `trellis-meta`; put project rules in `.trellis/spec/` or a project-local skill. -- Do not describe removed historical mechanisms as current Trellis behavior. +- Do not modify the global npm install directory or `node_modules/@mindfoldhq/trellis` or `node_modules/@mindfoldhq/trellis-core` to implement project needs; both packages ship in lockstep. +- Do not overwrite user-modified local files with default templates; check `.trellis/.template-hashes.json` first and prefer `.new` sidecar files over destructive overwrites. +- Do not put team-private project rules into any public bundled skill (`trellis-meta`, `trellis-spec-bootstrap`, `trellis-session-insight`, `trellis-channel`); put project rules in `.trellis/spec/`, a project-local skill, the current task, or the workspace journal — `trellis update` will overwrite anything inside a bundled skill directory. +- Do not hand-edit `~/.trellis/channels/<project>/<channel>/events.jsonl`; sequence numbers are assigned under a file lock and replay-safe writes go through the `trellis channel` CLI or the `@mindfoldhq/trellis-core/channel` SDK. +- Do not edit `.claude/agents/trellis-implement.md` (or any other per-platform sub-agent file) when the goal is to change channel runtime worker behavior — edit `.trellis/agents/<name>.md` instead. +- Do not describe removed or never-shipped mechanisms as current Trellis behavior; cross-check against the local `.trellis/config.yaml` and the installed CLI's `trellis --help` before claiming a knob exists. diff --git a/.agents/skills/trellis-meta/references/customize-local/change-agents.md b/.agents/skills/trellis-meta/references/customize-local/change-agents.md index 9b635310..860c34c3 100644 --- a/.agents/skills/trellis-meta/references/customize-local/change-agents.md +++ b/.agents/skills/trellis-meta/references/customize-local/change-agents.md @@ -24,6 +24,8 @@ When the user wants to change `trellis-research`, `trellis-implement`, or `trell | CodeBuddy | `.codebuddy/agents/trellis-*.md` | | Factory Droid | `.factory/droids/trellis-*.md` | | Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/agents/trellis-*.md` | Use the actual paths in the user project as authoritative. diff --git a/.agents/skills/trellis-meta/references/customize-local/change-context-loading.md b/.agents/skills/trellis-meta/references/customize-local/change-context-loading.md index 556b4e38..002a2595 100644 --- a/.agents/skills/trellis-meta/references/customize-local/change-context-loading.md +++ b/.agents/skills/trellis-meta/references/customize-local/change-context-loading.md @@ -18,6 +18,8 @@ Context loading determines when AI reads workflow, task, spec, research, workspa | --- | --- | | `.trellis/workflow.md` | Workflow and next-action hints. | | `.trellis/tasks/<task>/prd.md` | Current task requirements. | +| `.trellis/tasks/<task>/design.md` | Complex task technical design. | +| `.trellis/tasks/<task>/implement.md` | Complex task execution plan. | | `.trellis/tasks/<task>/implement.jsonl` | Spec/research to read before implementation. | | `.trellis/tasks/<task>/check.jsonl` | Spec/research to read during checking. | | `.trellis/spec/` | Project specs. | @@ -64,10 +66,11 @@ First determine which mode the platform uses: In both modes, make sure the agent ultimately reads: 1. active task -2. `prd.md` -3. `info.md` if present -4. the corresponding JSONL -5. spec/research referenced by the JSONL +2. the corresponding JSONL +3. spec/research referenced by the JSONL +4. `prd.md` +5. `design.md` if present +6. `implement.md` if present ## Troubleshooting Order diff --git a/.agents/skills/trellis-meta/references/customize-local/change-hooks.md b/.agents/skills/trellis-meta/references/customize-local/change-hooks.md index 5c1ed7a4..79aa5c58 100644 --- a/.agents/skills/trellis-meta/references/customize-local/change-hooks.md +++ b/.agents/skills/trellis-meta/references/customize-local/change-hooks.md @@ -4,7 +4,7 @@ Hooks are the automation layer that connects a platform to Trellis. When the use ## Read These Files First -1. Target platform settings/config, such as `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json` +1. Target platform settings/config, such as `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`, `.trae/hooks.json` 2. Target platform hooks directory 3. `.trellis/scripts/common/active_task.py` 4. `.trellis/scripts/common/session_context.py` diff --git a/.agents/skills/trellis-meta/references/customize-local/change-skills-or-commands.md b/.agents/skills/trellis-meta/references/customize-local/change-skills-or-commands.md index 84590a11..9e25effb 100644 --- a/.agents/skills/trellis-meta/references/customize-local/change-skills-or-commands.md +++ b/.agents/skills/trellis-meta/references/customize-local/change-skills-or-commands.md @@ -2,12 +2,20 @@ When the user wants to change AI entry points, auto-trigger rules, or explicit command behavior, edit skills, commands, prompts, or workflows in local platform directories. +Before editing, classify the skill you are about to touch: + +- **Bundled upstream skill** — `trellis-meta`, `trellis-spec-bootstrap`, `trellis-session-insight`, `trellis-channel`. Source of truth lives in the Trellis CLI repo under `packages/cli/src/templates/common/bundled-skills/<name>/`; auto-dispatched to every platform's skill root by `getBundledSkillTemplates()` on `trellis init` / `trellis update`. Local edits here are tracked by `.trellis/.template-hashes.json` and will be flagged on the next update. +- **Project-local skill** — anything else under `.{platform}/skills/`. Owned by the user; not refreshed by `trellis update`. + +The remainder of this file uses "skill" for the local file; the override and conflict rules differ between the two cases. + ## Read These Files First 1. `.trellis/workflow.md` 2. Target platform skill/command/prompt/workflow directory 3. Related agent or hook files 4. Whether project rules already exist in `.trellis/spec/` +5. `.trellis/.template-hashes.json` — confirms whether the skill you are about to edit is upstream-owned (entry present) or project-local (entry absent) ## Which Entry Type To Choose @@ -15,7 +23,9 @@ When the user wants to change AI entry points, auto-trigger rules, or explicit c | --- | --- | | AI should automatically know a capability | Add or modify a skill. | | User wants to trigger manually with a command | Add or modify a command/prompt/workflow. | -| Team project conventions | Prefer `.trellis/spec/` or a project-local skill. | +| Team project conventions | Prefer `.trellis/spec/` or a project-local skill — never a bundled skill directory. | +| Tweak a bundled skill (`trellis-meta` et al.) for the user's own project | Create a project-local sibling skill (different name) that overrides intent, or edit `.trellis/spec/`. Edits inside the bundled skill directory survive only until the next `trellis update` and will need a "keep" choice each time. | +| Contribute the change back upstream | Edit `packages/cli/src/templates/common/bundled-skills/<name>/` in the Trellis CLI repo, not the deployed copy. | | Change Trellis flow semantics | Synchronize `.trellis/workflow.md`. | ## Modify A Skill @@ -38,6 +48,20 @@ description: "Use when customizing this project's deployment workflow and releas Do not write vague descriptions such as "helpful project skill"; they can trigger incorrectly. +### Bundled vs. Project-Local + +The same directory shape is used by two very different ownership models: + +| Aspect | Bundled (`trellis-meta`, `trellis-spec-bootstrap`, `trellis-session-insight`, `trellis-channel`) | Project-local | +| --- | --- | --- | +| Source of truth | `packages/cli/src/templates/common/bundled-skills/<name>/` in Trellis CLI repo | Inside the user project itself | +| Dispatch | Auto-dispatched to every platform skill root by `getBundledSkillTemplates()` (`packages/cli/src/templates/common/index.ts`) on `trellis init` / `trellis update` | Created by the user (or another skill) and never moved | +| Hash tracking | Every file recorded in `.trellis/.template-hashes.json`; conflict prompt on update | Not tracked | +| Editing locally | Allowed but will be marked "modified by user" on next update | Free editing | +| The right way to customize | Add a *new* project-local skill with a *different* name that supplements (or supersedes) the bundled one | Edit the file directly | + +If the goal is "make my project's AI behave differently when discussing release notes," the answer is almost always a project-local skill, not surgery on `trellis-meta/`. + ## Modify A Command/Prompt/Workflow Explicit entry points should state: @@ -57,22 +81,43 @@ If a command only repeats workflow rules, prefer making it reference/read `.trel | Cursor | `.cursor/skills/`, `.cursor/commands/` | | OpenCode | `.opencode/skills/`, `.opencode/commands/` | | Codex | `.agents/skills/`, `.codex/skills/` | +| Gemini CLI | `.agents/skills/`, `.gemini/commands/` | +| Kiro | `.kiro/skills/` | +| Qoder | `.qoder/skills/`, `.qoder/commands/` | +| CodeBuddy | `.codebuddy/skills/`, `.codebuddy/commands/` | | GitHub Copilot | `.github/skills/`, `.github/prompts/` | -| Kilo / Antigravity / Windsurf | workflows + skills | +| Factory Droid | `.factory/skills/`, `.factory/commands/` | +| Pi Agent | `.agents/skills/` | +| Reasonix | `.reasonix/skills/` (no separate commands dir; slash commands built into the platform) | +| ZCode | `.zcode/skills/`, `.zcode/commands/` | +| Kilo / Antigravity / Devin | workflows + skills | + +Every directory above is a deploy target for the four bundled skills. Each platform receives a full copy on `trellis init` and refresh on `trellis update`; nothing has to be wired by hand. ## Add A Project-Local Skill -If the user wants to document team-private customizations, create a project-local skill, for example: +If the user wants to document team-private customizations, create a project-local skill — never put project-private content into a bundled skill directory, since `trellis update` will overwrite it. ```text .claude/skills/project-trellis-local/ └── SKILL.md ``` -For multi-platform projects, add equivalent versions in each platform skill directory, or use `.agents/skills/` on platforms that support the shared layer. +For multi-platform projects, add equivalent versions in each platform skill directory, or use `.agents/skills/` on platforms that support the shared layer (Codex, Gemini CLI). + +Pick a name that does **not** collide with the bundled set: + +- `trellis-meta` +- `trellis-spec-bootstrap` +- `trellis-session-insight` +- `trellis-channel` + +A reused name causes `getBundledSkillTemplates()` to overwrite the project-local copy on the next update. A common convention is to prefix the project name: `acme-trellis-deploy`, `acme-trellis-onboarding`. ## Notes - Do not mix every platform's syntax into one file. - Do not change only one platform entry point while claiming all platforms are supported. - Do not hide long-term engineering conventions inside a command; write them to `.trellis/spec/`. +- Do not hand-edit files inside `trellis-meta/`, `trellis-spec-bootstrap/`, `trellis-session-insight/`, or `trellis-channel/` under any `.{platform}/skills/` directory expecting the change to persist — they are bundled and refreshed by `trellis update`. Either contribute upstream or add a project-local skill that complements them. +- After `trellis update` reports a "modified by you" conflict on a bundled skill file, choose **keep** only if you accept maintaining the divergence by hand; otherwise accept the overwrite and re-apply the intent as a project-local skill. diff --git a/.agents/skills/trellis-meta/references/customize-local/change-spec-structure.md b/.agents/skills/trellis-meta/references/customize-local/change-spec-structure.md index 358de513..ee9a176b 100644 --- a/.agents/skills/trellis-meta/references/customize-local/change-spec-structure.md +++ b/.agents/skills/trellis-meta/references/customize-local/change-spec-structure.md @@ -6,7 +6,7 @@ When the user wants to change the engineering conventions AI follows, add new sp 1. `.trellis/config.yaml` 2. `.trellis/spec/` -3. `.trellis/workflow.md` Phase 1.3 and Phase 3.3 +3. `.trellis/workflow.md` planning artifact guidance and Phase 3.3 4. Current task `implement.jsonl` / `check.jsonl` ## Common Needs diff --git a/.agents/skills/trellis-meta/references/customize-local/change-workflow.md b/.agents/skills/trellis-meta/references/customize-local/change-workflow.md index 4231845a..337c9856 100644 --- a/.agents/skills/trellis-meta/references/customize-local/change-workflow.md +++ b/.agents/skills/trellis-meta/references/customize-local/change-workflow.md @@ -50,11 +50,12 @@ If the user wants only one platform to avoid sub-agents, first confirm whether t | `status` | Artifact state | Resume at | | --- | --- | --- | | `planning` | `prd.md` missing | Phase 1.1 (load `trellis-brainstorm`) | -| `planning` | `prd.md` exists, `implement.jsonl` only has the seed `_example` row | Phase 1.3 (curate JSONL context) | -| `planning` | `prd.md` exists, `implement.jsonl` curated | Phase 1.4 (run `task.py start`) | +| `planning` | lightweight task with `prd.md` complete | ask for start review, then run `task.py start` | +| `planning` | complex task missing `design.md` or `implement.md` | complete missing planning artifacts | +| `planning` | complex task has `prd.md`, `design.md`, and `implement.md` | ask for start review, then run `task.py start` | | `in_progress` | no implementation in conversation history | Phase 2.1 (`trellis-implement`) | | `in_progress` | implementation done, no `trellis-check` run | Phase 2.2 (`trellis-check`) | -| `in_progress` | check passed | Phase 3.1 (verify quality + spec update) | +| `in_progress` | check passed | Phase 3.3 (spec update) → 3.4 (commit) | | `completed` | task is still in active tree | Phase 3.5 (run `/trellis:finish-work` to archive) | When you add a custom status (e.g. `in-review`), add a `[workflow-state:in-review]` block in `.trellis/workflow.md` for the per-turn breadcrumb AND extend this route table — usually by editing the `/trellis:continue` command file (`.{platform}/commands/trellis/continue.md` or equivalent) to add a row that decides where to resume from. Without the route entry, `/trellis:continue` will fall through to a default branch and the user will not land on the step you intended. diff --git a/.agents/skills/trellis-meta/references/customize-local/overview.md b/.agents/skills/trellis-meta/references/customize-local/overview.md index b53b090f..b75d208d 100644 --- a/.agents/skills/trellis-meta/references/customize-local/overview.md +++ b/.agents/skills/trellis-meta/references/customize-local/overview.md @@ -17,7 +17,7 @@ This directory is for local AI working in a user project where Trellis was insta ## General Operation Order -1. **Confirm platform and directories**: inspect which directories exist, such as `.claude/`, `.codex/`, `.cursor/`. +1. **Confirm platform and directories**: inspect which directories exist, such as `.claude/`, `.codex/`, `.cursor/`, `.zcode/`. 2. **Confirm the current active task**: run `python3 ./.trellis/scripts/task.py current --source`. 3. **Read the local source of truth**: prefer `.trellis/workflow.md`, `.trellis/config.yaml`, and relevant platform files. 4. **Modify narrowly**: edit only files related to the user's request. @@ -32,7 +32,7 @@ This directory is for local AI working in a user project where Trellis was insta | Task material | `.trellis/tasks/<task>/` | | Project specs | `.trellis/spec/` | | Runtime scripts | `.trellis/scripts/` | -| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, and similar directories | +| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.zcode/`, and similar directories | | Shared skill | `.agents/skills/` | ## Things Not To Do By Default diff --git a/.agents/skills/trellis-meta/references/local-architecture/bundled-skills.md b/.agents/skills/trellis-meta/references/local-architecture/bundled-skills.md new file mode 100644 index 00000000..ae288703 --- /dev/null +++ b/.agents/skills/trellis-meta/references/local-architecture/bundled-skills.md @@ -0,0 +1,147 @@ +# Bundled Skills + +"Bundled skills" are multi-file built-in skills shipped inside the Trellis CLI npm package. Unlike marketplace skills (which a user installs separately into their own `.claude/skills/` or other platform skill root), bundled skills are written automatically into every supported platform's skill root by `trellis init` and kept in sync by `trellis update`. They are part of Trellis itself, not third-party content. + +A bundled skill is a directory under `packages/cli/src/templates/common/bundled-skills/<skill>/` that already contains its own `SKILL.md` (with YAML frontmatter) plus optional `references/`, assets, or other supporting files. Trellis copies the whole directory tree as-is into each platform's skill root, so references stay lazy-loadable instead of being flattened into one oversized `SKILL.md`. + +## What Counts As Bundled (vs. Adjacent Concepts) + +| Source path | Type | How it ships | +| --- | --- | --- | +| `templates/common/bundled-skills/<name>/` | Bundled skill (multi-file) | Whole directory copied to every platform skill root | +| `templates/common/skills/<name>.md` | Single-file workflow skill | Wrapped with frontmatter, written as `<root>/<name>/SKILL.md` | +| `templates/common/commands/<name>.md` | Slash command / prompt | Written to each platform's command directory (`.claude/commands/trellis/`, `.cursor/commands/trellis-*.md`, `.gemini/commands/trellis/*.toml`, etc.) | +| `templates/<platform>/skills/` | Platform-specific skill | Written only into that platform's directory (e.g. `.codex/skills/`) | +| User skills under `.claude/skills/<my-skill>/` etc. | Marketplace or user-authored | Not managed by Trellis at all | + +The Trellis CLI never touches anything that is not produced by one of its own template loaders. Anything a user drops into a platform skill root by hand is left alone. + +## Current Bundled Skills (v0.6.0) + +The set is discovered at runtime by listing directories under `templates/common/bundled-skills/`: + +| Skill | Purpose | +| --- | --- | +| `trellis-meta` | This skill. Explains the local Trellis architecture and customization entry points to an AI working inside a user project. | +| `trellis-session-insight` | Wraps the `trellis mem` CLI so an AI knows when and how to reach into past Claude Code / Codex / Pi Agent conversation logs. | +| `trellis-spec-bootstrap` | Platform-neutral workflow for creating or refreshing `.trellis/spec/` from the real codebase (with optional GitNexus / ABCoder integration). | +| `trellis-channel` | Capability skill teaching an AI when to reach for `trellis channel` for multi-agent collaboration, forum/thread persistent boards, and dispatcher-wait patterns. | + +The list is discovered at runtime, so adding a new directory under `bundled-skills/` is the only step required to register a new skill (see "Adding a New Bundled Skill" below). + +## Where Bundled Skills Land Per Platform + +Each platform configurator calls `writeSkills(<root>, <workflowSkills>, resolveBundledSkills(ctx))` during `trellis init`. `resolveBundledSkills` reads every directory under `templates/common/bundled-skills/`, resolves placeholders, and returns a flat list of `{relativePath, content}` entries. `writeSkills` then mirrors them under the platform's skill root. + +| Platform | Bundled skill root | Notes | +| --- | --- | --- | +| Claude Code | `.claude/skills/<skill>/` | `configureClaude` | +| Cursor | `.cursor/skills/<skill>/` | `configureCursor` | +| Codex | `.agents/skills/<skill>/` | `configureCodex` writes the shared `.agents/skills/` root, which Gemini CLI 0.40+ also reads | +| Gemini CLI | `.agents/skills/<skill>/` | Same shared root as Codex; the two configurators are required to produce byte-identical output | +| Kiro | `.kiro/skills/<skill>/` | `configureKiro` (skills-based platform — no commands) | +| Qoder | `.qoder/skills/<skill>/` | `configureQoder` | +| Codebuddy | `.codebuddy/skills/<skill>/` | `configureCodebuddy` | +| Copilot | `.github/skills/<skill>/` | `configureCopilot` | +| Droid | `.factory/skills/<skill>/` | `configureDroid` | +| Antigravity | `.agent/skills/<skill>/` | `configureAntigravity` | +| Devin | `.devin/skills/<skill>/` | `configureDevin` | +| Kilo | `.kilocode/skills/<skill>/` | `configureKilo` | +| ZCode | `.zcode/skills/<skill>/` | `configureZcode` | +| OpenCode | (handled by `collectOpenCodeTemplates`) | Uses the same `resolveBundledSkills(ctx)` output | +| Pi, Reasonix | (their own collectors) | Same `resolveBundledSkills(ctx)` output | + +Two paths exercise the same data: + +1. `configureX(cwd)` writes files during `trellis init`. +2. `collectPlatformTemplates(platformId)` (in `configurators/index.ts`) returns a `Map<filePath, content>` that `trellis update` uses to detect drift and to populate `.trellis/.template-hashes.json`. Both must produce byte-identical output, so they both call `resolveBundledSkills(ctx)` and `collectSkillTemplates(root, …, resolveBundledSkills(ctx))`. + +## Dispatch Wiring (Code Path) + +The mechanism that auto-dispatches bundled skills to platform skill roots lives in two files: + +1. `packages/cli/src/templates/common/index.ts` + - `listDirectories("bundled-skills")` enumerates the on-disk skills. + - `listBundledSkillFiles(skillDir)` walks each skill's directory recursively and returns `{relativePath, content}` for every file. + - `getBundledSkillTemplates()` returns the cached `CommonBundledSkill[]`. + +2. `packages/cli/src/configurators/shared.ts` + - `resolveBundledSkills(ctx)` flattens that list into `ResolvedSkillFile[]` with `<skill>/<relativePath>` paths and resolved placeholders. + - `writeSkills(skillsRoot, workflowSkills, bundledSkills)` writes both workflow skills and bundled skill files under `skillsRoot`. + - `collectSkillTemplates(skillsRoot, workflowSkills, bundledSkills)` returns the same shape as a `Map<filePath, content>` for the update / hash pipeline. + +Every platform configurator that supports skills imports both helpers (see `claude.ts`, `cursor.ts`, `codex.ts`, `gemini.ts`, `kiro.ts`, `qoder.ts`, `codebuddy.ts`, `copilot.ts`, `droid.ts`, `antigravity.ts`, `devin.ts`, `kilo.ts`). The `index.ts` `PLATFORM_FUNCTIONS` registry also calls `resolveBundledSkills(ctx)` inside each `collectTemplates` closure so `trellis update` tracking stays consistent. + +## Adding a New Bundled Skill + +The shape and dispatch wiring are already generic, so adding a skill requires only file changes plus distribution verification. + +1. **Create the directory tree.** + + ``` + packages/cli/src/templates/common/bundled-skills/<my-skill>/ + SKILL.md # YAML frontmatter + body + references/ # optional + <topic>.md + assets/ # optional (anything readable as utf-8) + ``` + +2. **Write a valid `SKILL.md` header.** The frontmatter must include at minimum: + + ```yaml + --- + name: <my-skill> + description: "When the AI should reach for this skill. Triggering phrases go here." + --- + ``` + + The `description` is what each platform's auto-trigger mechanism matches against, so it should describe the user-intent triggers, not the skill's internals. + +3. **Use placeholders where appropriate.** Bundled skill content runs through `resolvePlaceholders(file.content, ctx)`. Any `{{platform_name}}`, `{{python_cmd}}`, etc. token supported by `resolvePlaceholders` will be substituted per platform. + +4. **No dispatch wiring is required.** `listDirectories("bundled-skills")` discovers the new directory automatically, so all platforms receive it on the next `trellis init` or `trellis update`. + +5. **Verify the distribution path** before shipping. Skipping any of these steps has historically caused features to be documented as bundled while the published npm tarball was missing the files: + + - Source files exist on the branch being tagged. + - `pnpm --filter @mindfoldhq/trellis build` copies the asset into `dist/templates/common/bundled-skills/<skill>/`. + - `npm pack --dry-run --json` includes the expected `dist/**` paths. + - In a fresh temp project, `trellis init` writes `.claude/skills/<skill>/SKILL.md`, `.agents/skills/<skill>/SKILL.md`, `.zcode/skills/<skill>/SKILL.md`, etc. + - `.trellis/.template-hashes.json` lists the generated files. + - `trellis update --dry-run` in that temp project reports "Already up to date!". + +6. **Add a migration manifest entry** if the skill is added in a release that other projects will upgrade into. Without an explicit manifest entry the file will land via the standard "missing file" branch of `trellis update`, but a manifest makes the change visible in the changelog. + +## Overriding a Bundled Skill Locally + +There is no formal "project-local skill" mechanism (e.g. `.trellis/skills/`). Bundled skills are platform-rooted, so any override is platform-rooted too. + +The supported pattern relies on the existing template-hash diff in `trellis update`: + +1. Edit the local file directly. Example: `.claude/skills/trellis-meta/SKILL.md`. +2. The file's hash now diverges from the entry in `.trellis/.template-hashes.json`. +3. The next `trellis update` detects the user modification and leaves the file untouched (Trellis never overwrites user-modified files without an explicit `--force`). + +Caveats: + +- The override only applies to the one platform whose directory you edited. To override the same skill across, for example, Claude Code and Codex, you must edit both `.claude/skills/<name>/` and `.agents/skills/<name>/`. +- A future `trellis update --force` will overwrite local edits. Keep the override under version control so it can be reapplied if needed. +- Marketplace skills installed under the same platform skill root with a different folder name (e.g. `.claude/skills/my-custom-meta/`) are untouched by Trellis and are the cleaner option when the goal is to add behavior, not to mutate the bundled skill. +- Team-private conventions belong in `.trellis/spec/` or in a separate marketplace-style local skill, not in modifications to `trellis-meta` itself. See `customize-local/add-project-local-conventions.md`. + +## Removing a Bundled Skill From a Project + +There is no per-project opt-out flag for bundled skills. Two options: + +1. **Delete the directory in each platform skill root.** `trellis update` will see the file missing, compare against `.template-hashes.json`, and treat the deletion the same as any other user modification — it will not silently re-create the directory unless `--force` is passed. + +2. **Pin a Trellis version that did not ship the skill.** The bundled-skill set is determined at build time, so installing an older release of the CLI is the only way to permanently exclude a skill that the current release ships. + +A third option — globally disabling all bundled skills — is not supported. The dispatch is unconditional in every configurator. Adding such a flag would require changing `PLATFORM_FUNCTIONS` in `configurators/index.ts` and every `configureX` function. + +## Operating Rules + +- Treat `templates/common/bundled-skills/` as the single source of truth for what bundled skills exist. Do not hand-maintain platform-by-platform skill lists. +- Do not add platform-specific logic inside a bundled `SKILL.md`. If a behavior is platform-specific, put it in `templates/<platform>/skills/` instead. +- Do not couple bundled skills to a specific CLI binary (e.g. `trellis mem`) without surfacing the dependency in the skill's description and references — users on older releases may not have the command. +- Do not store project-private content in a bundled skill. Bundled skills are public, shipped to every user; project rules belong in `.trellis/spec/` or a local skill. diff --git a/.agents/skills/trellis-meta/references/local-architecture/context-injection.md b/.agents/skills/trellis-meta/references/local-architecture/context-injection.md index fae6fa58..4a7517bb 100644 --- a/.agents/skills/trellis-meta/references/local-architecture/context-injection.md +++ b/.agents/skills/trellis-meta/references/local-architecture/context-injection.md @@ -9,7 +9,7 @@ Trellis context injection aims to make AI read the right files at the right time | session context | `.trellis/scripts/get_context.py` | Current developer, git status, active task, active tasks, journal, packages. | | workflow context | `.trellis/workflow.md` | Current Trellis flow and next action. | | spec context | `.trellis/spec/` + task JSONL | Specs that must be followed during implementation/checking. | -| task context | `.trellis/tasks/<task>/prd.md`, `info.md`, `research/` | Current task requirements, design, and research. | +| task context | `.trellis/tasks/<task>/prd.md`, `design.md`, `implement.md`, `research/` | Current task requirements, design, execution plan, and research. | | platform context | Platform hooks/settings/agents | Lets different AI tools read the files above through their own mechanisms. | ## session-start @@ -34,10 +34,10 @@ If the user wants to change "what the AI should do next in a given state," edit Implement and check agents need task context. Trellis has two loading modes: -1. **hook push**: a platform hook injects `prd.md` and the files referenced by `implement.jsonl` / `check.jsonl` before the agent starts. -2. **agent pull**: the agent definition instructs the agent to read the active task, PRD, and JSONL context after startup. +1. **hook push**: a platform hook injects jsonl-referenced files plus `prd.md`, `design.md` if present, and `implement.md` if present before the agent starts. +2. **agent pull**: the agent definition instructs the agent to read the active task, jsonl context, and task artifacts after startup. -In both modes, JSONL files in the task directory are the key interface. +In both modes, JSONL files in the task directory are the manifest for spec/research context. Task artifacts are read separately in this order: `prd.md` -> `design.md if present` -> `implement.md if present`. ## JSONL Reading Rules @@ -65,4 +65,4 @@ If shell commands cannot see the same context key, `task.py current --source` ma | Change JSONL validation/display | `.trellis/scripts/common/task_context.py`. | | Change active task resolution | `.trellis/scripts/common/active_task.py`. | -When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct PRD/spec/research. +When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct task artifacts/spec/research. diff --git a/.agents/skills/trellis-meta/references/local-architecture/generated-files.md b/.agents/skills/trellis-meta/references/local-architecture/generated-files.md index 66f832d7..b4602244 100644 --- a/.agents/skills/trellis-meta/references/local-architecture/generated-files.md +++ b/.agents/skills/trellis-meta/references/local-architecture/generated-files.md @@ -40,10 +40,10 @@ Different platforms generate different directories. Common categories: | Category | Example paths | Purpose | | --- | --- | --- | | hooks | `.claude/hooks/`, `.codex/hooks/`, `.cursor/hooks/` | Inject session context, workflow-state, and sub-agent context. | -| settings | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json` | Tell the platform when to run hooks or plugins. | -| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/` | Define agents such as `trellis-research`, `trellis-implement`, and `trellis-check`. | -| skills | `.claude/skills/`, `.agents/skills/`, `.qoder/skills/` | Skills that auto-trigger or can be read by AI. | -| commands/prompts/workflows | `.cursor/commands/`, `.github/prompts/`, `.windsurf/workflows/` | Explicit user-invoked command or workflow entry points. | +| settings | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.trae/hooks.json` | Tell the platform when to run hooks or plugins. | +| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/`, `.zcode/agents/` | Define agents such as `trellis-research`, `trellis-implement`, and `trellis-check`. | +| skills | `.claude/skills/`, `.agents/skills/`, `.qoder/skills/`, `.zcode/skills/` | Skills that auto-trigger or can be read by AI. | +| commands/prompts/workflows | `.cursor/commands/`, `.github/prompts/`, `.devin/workflows/`, `.zcode/commands/` | Explicit user-invoked command or workflow entry points. | When modifying a platform directory, also confirm whether `.trellis/workflow.md` still describes the same flow. diff --git a/.agents/skills/trellis-meta/references/local-architecture/multi-agent-channel.md b/.agents/skills/trellis-meta/references/local-architecture/multi-agent-channel.md new file mode 100644 index 00000000..6df61eb6 --- /dev/null +++ b/.agents/skills/trellis-meta/references/local-architecture/multi-agent-channel.md @@ -0,0 +1,69 @@ +# Local Multi-Agent Channel Runtime + +`trellis channel` is the local multi-agent collaboration runtime shipped with the Trellis CLI. It lets the main AI session spawn peer workers (Claude Code, Codex, or any agent definition under `.trellis/agents/`), exchange durable messages through an event log, and coordinate review or brainstorm loops without hand-stitching shell pipelines. + +This reference covers how channels are wired into the user project so an AI customizing the project knows what to edit. For runtime usage (commands, forum/thread patterns, worker spawn flags), defer to the bundled `trellis-channel` capability skill. + +## Local System Model + +The channel runtime spans three local surfaces: + +1. **Storage layer** in the user's home directory: durable event logs and worker state files. +2. **Agent definitions** inside the project at `.trellis/agents/`: platform-agnostic role cards consumed by `trellis channel spawn --agent <name>`. +3. **Project configuration** in `.trellis/config.yaml`: worker guard thresholds and other channel knobs. + +## Core Paths + +| Path | Purpose | +| --- | --- | +| `~/.trellis/channels/<project>/<channel>/events.jsonl` | Per-channel append-only event log. Sequence-locked, replay-safe. | +| `~/.trellis/channels/<project>/<channel>/<channel>.lock` | Channel-level write lock. | +| `~/.trellis/channels/<project>/<channel>/<worker>.spawnlock` | Per-worker spawn lock used by the OOM guard. | +| `~/.trellis/channels/<project>/<channel>/.seq` | Sequence sidecar for ordered event assignment. | +| `~/.trellis/channels/_global/<channel>/...` | Channels created with `--scope global`. The project bucket is replaced by a shared key. | +| `.trellis/agents/check.md` | Default Check Agent role definition consumed by `--agent check`. | +| `.trellis/agents/implement.md` | Default Implement Agent role definition consumed by `--agent implement`. | +| `.trellis/config.yaml` (`channel.*` block) | Worker guard thresholds and channel defaults. | + +The project bucket name is derived from the absolute project path (slashes flattened, non-alphanumerics replaced with `-`), matching Claude Code's `~/.claude/projects/<sanitized-cwd>/` convention. Override with `TRELLIS_CHANNEL_ROOT` (root directory) or `TRELLIS_CHANNEL_PROJECT` (bucket name) for testing or sandboxing. + +## When To Reach For The Channel Runtime + +Channels are heavier than a single Bash call or a one-shot sub-agent dispatch. Use them only when at least one of these conditions holds: + +- The work needs **two or more agents to converse** through more than one turn (cross-AI brainstorm, peer review, dispatcher + worker). +- A worker should run as a **peer process** that the main session can interrupt, watch progress on, or wait for asynchronously. +- The conversation must be **durable and inspectable** later (forum/thread channels, issue boards, decision trails). +- Multiple workers must **share an event log** so each can see what the others reported. + +Prefer cheaper primitives when: + +- A single-shot Bash command or single Agent tool call is enough -> do that directly. +- The user just needs a static review against a file -> read the file and reply inline. +- The need is "remember what we discussed last week" -> use `trellis mem` instead of a channel. + +## Customization Points + +| Need | Edit location | +| --- | --- | +| Change default channel worker idle timeout | `channel.worker_guard.idle_timeout` in `.trellis/config.yaml`. Accepts `5m`, `30s`, etc. Set `0` to disable idle cleanup. | +| Change live worker budget | `channel.worker_guard.max_live_workers` in `.trellis/config.yaml`. Set `0` to disable the spawn-time budget check. | +| Override worker guard per spawn | Pass `--idle-timeout` / `--max-live-workers` on `trellis channel spawn`, or set `TRELLIS_CHANNEL_WORKER_IDLE_TIMEOUT` / `TRELLIS_CHANNEL_MAX_LIVE_WORKERS` in the environment. | +| Change what the default Check or Implement worker does | Edit `.trellis/agents/check.md` or `.trellis/agents/implement.md`. These are platform-agnostic role cards; the channel runtime injects them when `--agent check|implement` is passed. | +| Add a new role card | Drop `<name>.md` into `.trellis/agents/`. `trellis channel spawn --agent <name>` will pick it up. | +| Relocate channel storage (CI sandbox, ephemeral runs) | Set `TRELLIS_CHANNEL_ROOT=/path/to/dir`. Channel events move with it; existing channels stay at the old root. | +| Switch storage scope | Pass `--scope project` (default) or `--scope global` on every channel subcommand. The bucket directory changes; nothing else does. | + +Precedence for the worker guard is: CLI flag > environment variable > `.trellis/config.yaml` > built-in default. Built-in defaults are `idle_timeout: 5m` and `max_live_workers: 6`. + +## Relationship To Other Local Layers + +- **Workflow layer**: workflows that use channel dispatch (such as `channel-driven-subagent-dispatch`) instruct the main agent to call `trellis channel spawn --agent check` or `--agent implement` instead of a platform sub-agent. If `.trellis/agents/check.md` or `implement.md` is missing, `trellis workflow --template <id>` prints a non-blocking warning at install time. Restore them with `trellis update` if they are deleted by accident. +- **Task layer**: channel workers do not own task state. The supervising main session passes the active task path through the worker inbox; the worker resolves task artifacts from disk. +- **Spec layer**: workers read `.trellis/spec/` the same way the main session does. Channel runtime does not bypass spec context loading. +- **Platform integration layer**: channel runtime is platform-neutral. It does not depend on `.claude/`, `.codex/`, or any other platform directory. The adapters that normalize provider output (Claude `stream-json`, Codex `app-server`) live inside the Trellis CLI binary, not in the project. +- **Platform sub-agent files vs. channel workers**: editing `.claude/agents/trellis-implement.md` (and its peers in other platform `.X/agents/` directories) does NOT change channel-runtime worker behavior — channel workers load `.trellis/agents/<name>.md`. The platform-specific agent files are for direct sub-agent dispatch from the main AI session, not for channel-spawned workers. See `platform-files/agents.md` for the per-platform agent surface, and the `trellis-meta/SKILL.md` rule that codifies this split. + +## Runtime Usage + +For command syntax, forum/thread patterns, worker handles, progress inspection, and the `--kind done` / `--kind turn_finished` dispatcher wait pattern, load the bundled `trellis-channel` skill (auto-installed under each platform's skills directory after `trellis init` / `trellis update`). This reference only covers the local file layout and customization knobs; it does not duplicate command syntax that may change between releases. diff --git a/.agents/skills/trellis-meta/references/local-architecture/overview.md b/.agents/skills/trellis-meta/references/local-architecture/overview.md index 99c7f736..e97cab86 100644 --- a/.agents/skills/trellis-meta/references/local-architecture/overview.md +++ b/.agents/skills/trellis-meta/references/local-architecture/overview.md @@ -5,7 +5,7 @@ Therefore, when an AI uses this skill, the default customization target is local files inside the user project: - `.trellis/`: workflow, tasks, specs, memory, scripts, and runtime state. -- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.devin/`, `.reasonix/`, `.zcode/`, and similar directories. - Shared skill layer: `.agents/skills/`. Do not default to guiding the user to fork the Trellis CLI repository. Treat upstream source code as the operating target only when the user explicitly says they want to change Trellis upstream source, publish an npm package, or contribute a PR. diff --git a/.agents/skills/trellis-meta/references/local-architecture/spec-system.md b/.agents/skills/trellis-meta/references/local-architecture/spec-system.md index 61281f06..38fdf143 100644 --- a/.agents/skills/trellis-meta/references/local-architecture/spec-system.md +++ b/.agents/skills/trellis-meta/references/local-architecture/spec-system.md @@ -65,7 +65,7 @@ This command lists packages and spec layers for the current project. Use this ou ## How Specs Enter Tasks -Before a task enters implementation, Phase 1.3 should write relevant specs into `implement.jsonl` / `check.jsonl`: +Before a task enters implementation, planning may write relevant specs into `implement.jsonl` / `check.jsonl` when the task needs spec or research context beyond the task artifacts: ```jsonl {"file": ".trellis/spec/cli/backend/index.md", "reason": "CLI backend conventions"} diff --git a/.agents/skills/trellis-meta/references/local-architecture/task-system.md b/.agents/skills/trellis-meta/references/local-architecture/task-system.md index 64ad00dd..71334958 100644 --- a/.agents/skills/trellis-meta/references/local-architecture/task-system.md +++ b/.agents/skills/trellis-meta/references/local-architecture/task-system.md @@ -9,7 +9,8 @@ The Trellis task system is stored entirely under `.trellis/tasks/` in the user p ├── 04-28-example-task/ │ ├── task.json │ ├── prd.md -│ ├── info.md +│ ├── design.md +│ ├── implement.md │ ├── implement.jsonl │ ├── check.jsonl │ └── research/ @@ -20,8 +21,9 @@ The Trellis task system is stored entirely under `.trellis/tasks/` in the user p | File | Purpose | | --- | --- | | `task.json` | Task metadata: status, assignee, priority, branch, parent/child tasks, and similar fields. | -| `prd.md` | Requirements document; the most important business context during implementation. | -| `info.md` | Optional technical design. | +| `prd.md` | Requirements, constraints, and acceptance criteria. Lightweight tasks may be PRD-only. | +| `design.md` | Technical design for complex tasks: boundaries, contracts, data flow, compatibility, tradeoffs. | +| `implement.md` | Execution plan for complex tasks: ordered checklist, validation commands, review gates, rollback points. | | `implement.jsonl` | List of spec/research files the implement agent must read first. | | `check.jsonl` | List of spec/research files the check agent must read first. | | `research/` | Research artifacts. Complex findings should not live only in chat. | @@ -42,7 +44,34 @@ The Trellis task system is stored entirely under `.trellis/tasks/` in the user p | `commit` / `pr_url` | Commit and PR information after completion. | | `meta` | Extension fields. | -The AI should not treat phase numbers as task status. Task progress is mainly determined by `status`, `prd.md`, whether JSONL context is configured, and the phase descriptions in `workflow.md`. +## Parent / Child Task Trees + +Parent/child task relationships are for work structure. A parent task groups related deliverables under one source requirement set; it is not a dependency scheduler and does not replace the child task's own planning artifacts. + +Use a parent task when a request has multiple independently verifiable deliverables. The parent owns: + +- Source requirements and user-facing scope. +- The map of child tasks and their responsibility boundaries. +- Cross-child acceptance criteria and final integration review. + +Use child tasks for deliverables that can move through planning, implementation, check, and archive independently. If one child depends on another, write that dependency in the child `prd.md` / `implement.md`; do not rely on tree position to imply ordering. + +Create new children with: + +```bash +python3 ./.trellis/scripts/task.py create "<child title>" --slug <child-slug> --parent <parent-dir> +``` + +Link or unlink existing tasks with: + +```bash +python3 ./.trellis/scripts/task.py add-subtask <parent-dir> <child-dir> +python3 ./.trellis/scripts/task.py remove-subtask <parent-dir> <child-dir> +``` + +`children` on the parent is a historical list. When a child is archived, Trellis keeps that child name in the parent so progress like `[2/3 done]` remains meaningful after completed children move to `archive/`. + +The AI should not treat phase numbers as task status. Task progress is mainly determined by `status`, artifact presence (`prd.md`, optional `design.md` / `implement.md`), whether JSONL context is configured for sub-agent mode, and the phase descriptions in `workflow.md`. ## Active Task @@ -58,7 +87,7 @@ If the platform or shell environment has no stable session identity, `task.py st ## JSONL Context -`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first. +`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first. They do not replace `implement.md`; `implement.md` is the human-readable execution plan. Format: @@ -95,7 +124,7 @@ When modifying the task system, the AI should prefer script commands to maintain | Change the default task template | `.trellis/scripts/common/task_store.py` and task creation instructions. | | Change status semantics | `.trellis/workflow.md`, workflow-state hook logic, and task usage conventions. | | Add task lifecycle actions | `hooks.after_*` in `.trellis/config.yaml`. | -| Change context rules | Phase 1.3 in `.trellis/workflow.md` and related platform agent/hook instructions. | +| Change context rules | Planning artifact guidance in `.trellis/workflow.md` and related platform agent/hook instructions. | | Change archive policy | `.trellis/scripts/common/task_store.py` / `task_utils.py`. | These are local files in the user project. Do not default to editing Trellis CLI source code unless the user wants to contribute upstream. diff --git a/.agents/skills/trellis-meta/references/platform-files/agents.md b/.agents/skills/trellis-meta/references/platform-files/agents.md index efbacfa0..26f472ab 100644 --- a/.agents/skills/trellis-meta/references/platform-files/agents.md +++ b/.agents/skills/trellis-meta/references/platform-files/agents.md @@ -13,7 +13,7 @@ File locations and formats differ by platform, but responsibility boundaries sho | Agent | Responsibility | | --- | --- | | `trellis-research` | Investigate the question and write findings into the current task's `research/`. | -| `trellis-implement` | Implement against `prd.md`, `info.md`, `implement.jsonl`, and related spec/research. | +| `trellis-implement` | Implement against `prd.md`, optional `design.md` / `implement.md`, `implement.jsonl`, and related spec/research. | | `trellis-check` | Review changes, fix discovered issues, and run necessary checks. | Agent files should not become generic chat prompts. They should define input sources, write boundaries, whether code may be changed, and how results are reported. @@ -32,10 +32,13 @@ Agent files should not become generic chat prompts. They should define input sou | CodeBuddy | `.codebuddy/agents/trellis-*.md` | | Factory Droid | `.factory/droids/trellis-*.md` | | Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/agents/trellis-*.md` | +| Kimi Code | `.kimi-code/skills/trellis-*/SKILL.md` (agent prompts as skills, dispatched to the built-in `coder`; research needs its file-editing tools to persist findings) | GitHub Copilot agent/prompt support is provided by a combination of directories such as `.github/agents/`, `.github/prompts/`, and `.github/skills/`; inspect the files actually generated in the user project. -Main-session workflow platforms such as Kilo, Antigravity, and Windsurf may not have Trellis sub-agent files. They usually rely on workflows/skills to guide the main session. +Main-session workflow platforms such as Kilo, Antigravity, and Devin may not have Trellis sub-agent files. They usually rely on workflows/skills to guide the main session. ## Two Context Loading Modes @@ -50,10 +53,11 @@ Common on platforms that support agent hooks. The agent file instructs the agent to read after startup: - `python3 ./.trellis/scripts/task.py current --source` -- current task `prd.md` -- `info.md` - `implement.jsonl` or `check.jsonl` - spec/research files referenced by JSONL +- current task `prd.md` +- `design.md` if present +- `implement.md` if present This mode fits platforms whose hooks cannot reliably rewrite sub-agent prompts. @@ -70,7 +74,7 @@ This mode fits platforms whose hooks cannot reliably rewrite sub-agent prompts. ## Modification Principles 1. **Keep responsibilities single-purpose**. Do not mix research, implement, and check responsibilities into one agent. -2. **Specify the read order**. Agents must know to start from the active task and then find the PRD and JSONL. +2. **Specify the read order**. Agents must know to start from the active task, read jsonl/spec context, then read `prd.md`, `design.md` if present, and `implement.md` if present. 3. **Specify write boundaries**. Research usually only writes `research/`; implement can write code; check can fix issues. 4. **Keep semantics synchronized in multi-platform projects**. If the user configured Claude, Codex, and Cursor together, decide whether changes to one platform's agent also need to be applied to others. diff --git a/.agents/skills/trellis-meta/references/platform-files/hooks-and-settings.md b/.agents/skills/trellis-meta/references/platform-files/hooks-and-settings.md index 94156a88..a2ff389c 100644 --- a/.agents/skills/trellis-meta/references/platform-files/hooks-and-settings.md +++ b/.agents/skills/trellis-meta/references/platform-files/hooks-and-settings.md @@ -27,6 +27,9 @@ Common files: | GitHub Copilot | `.github/copilot/hooks.json` | | Factory Droid | `.factory/settings.json` | | Pi Agent | `.pi/settings.json`, `.pi/extensions/trellis/` | +| Trae IDE | `.trae/hooks.json` | + +Reasonix is a pull-based platform whose agent files contain prelude instructions to read context after startup. ZCode uses `.zcode/config.json` with shared hooks, including PreToolUse for sub-agent prompt injection. Kimi Code is likewise pull-based and has no project-level settings/hooks file Trellis writes (hooks live only in the user-level `~/.kimi-code/config.toml`), so its agent prompts ship as skills with the same prelude. Whether these files exist in a project depends on which `trellis init --<platform>` flags the user ran. diff --git a/.agents/skills/trellis-meta/references/platform-files/overview.md b/.agents/skills/trellis-meta/references/platform-files/overview.md index 60ae1dfb..f9b72b04 100644 --- a/.agents/skills/trellis-meta/references/platform-files/overview.md +++ b/.agents/skills/trellis-meta/references/platform-files/overview.md @@ -5,7 +5,7 @@ Trellis connects the same local architecture to different AI tools. `.trellis/` When a local AI modifies Trellis, it should distinguish two file categories first: - **Shared files**: `.trellis/workflow.md`, `.trellis/tasks/`, `.trellis/spec/`, `.trellis/scripts/`. -- **Platform files**: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories. +- **Platform files**: `.claude/`, `.snow/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.trae/`, `.kilocode/`, `.agent/`, `.devin/`, `.reasonix/`, `.zcode/`, `.kimi-code/`, and similar directories. Platform files do not store business state. They let the corresponding AI tool read Trellis state, call Trellis scripts, and load Trellis skills/agents/hooks. @@ -13,11 +13,11 @@ Platform files do not store business state. They let the corresponding AI tool r | Category | Common paths | Purpose | | --- | --- | --- | -| settings/config | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json` | Register hooks, plugins, extensions, or platform behavior. | +| settings/config | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.trae/hooks.json` | Register hooks, plugins, extensions, or platform behavior. | | hooks/plugins/extensions | `.claude/hooks/`, `.opencode/plugins/`, `.pi/extensions/` | Inject context at session start, user input, agent startup, shell execution, and similar events. | -| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/` | Define `trellis-research`, `trellis-implement`, and `trellis-check`. | -| skills | `.claude/skills/`, `.agents/skills/`, `.qoder/skills/` | Capability descriptions that auto-trigger or can be read on demand. | -| commands/prompts/workflows | `.cursor/commands/`, `.github/prompts/`, `.windsurf/workflows/` | Entry points explicitly invoked by the user. | +| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/`, `.zcode/agents/` | Define `trellis-research`, `trellis-implement`, and `trellis-check`. | +| skills | `.claude/skills/`, `.agents/skills/`, `.qoder/skills/`, `.zcode/skills/` | Capability descriptions that auto-trigger or can be read on demand. | +| commands/prompts/workflows | `.cursor/commands/`, `.github/prompts/`, `.devin/workflows/`, `.zcode/commands/` | Entry points explicitly invoked by the user. | ## Three Platform Integration Modes diff --git a/.agents/skills/trellis-meta/references/platform-files/platform-map.md b/.agents/skills/trellis-meta/references/platform-files/platform-map.md index b5576f42..73b113a6 100644 --- a/.agents/skills/trellis-meta/references/platform-files/platform-map.md +++ b/.agents/skills/trellis-meta/references/platform-files/platform-map.md @@ -14,12 +14,18 @@ This page lists common Trellis file locations in a user project by platform. Whe | Kiro | `--kiro` | `.kiro/` | `.kiro/skills/` | `.kiro/agents/` | `.kiro/hooks/` | | Gemini CLI | `--gemini` | `.gemini/` | `.agents/skills/` | `.gemini/agents/` | `.gemini/settings.json` + `.gemini/hooks/` | | Antigravity | `--antigravity` | `.agent/` | `.agent/skills/` | Usually none | `.agent/workflows/` | -| Windsurf | `--windsurf` | `.windsurf/` | `.windsurf/skills/` | Usually none | `.windsurf/workflows/` | +| Devin | `--devin` | `.devin/` | `.devin/skills/` | Usually none | `.devin/workflows/` | | Qoder | `--qoder` | `.qoder/` | `.qoder/skills/` | `.qoder/agents/` | `.qoder/hooks/` + `.qoder/settings.json` | | CodeBuddy | `--codebuddy` | `.codebuddy/` | `.codebuddy/skills/` | `.codebuddy/agents/` | `.codebuddy/hooks/` + `.codebuddy/settings.json` | | GitHub Copilot | `--copilot` | `.github/` | `.github/skills/` | `.github/agents/` | `.github/copilot/hooks/` + prompts | | Factory Droid | `--droid` | `.factory/` | `.factory/skills/` | `.factory/droids/` | `.factory/hooks/` + settings | -| Pi Agent | `--pi` | `.pi/` | `.pi/skills/` | `.pi/agents/` | `.pi/extensions/trellis/` + `.pi/settings.json` | +| Pi Agent | `--pi` | `.pi/` | `.agents/skills/` | `.pi/agents/` | `.pi/extensions/trellis/` (native `trellis_subagent` tool) + `.pi/settings.json` | +| Trae IDE | `--trae` | `.trae/` | `.trae/skills/` | `.trae/agents/` | `.trae/hooks/` + `.trae/hooks.json` | +| Reasonix | `--reasonix` | `.reasonix/` | `.reasonix/skills/` | None — sub-agents are skills with `runAs: subagent` frontmatter | None | +| ZCode | `--zcode` | `.zcode/` | `.zcode/skills/` | `.zcode/agents/` | `.zcode/hooks/` + `.zcode/config.json` (SessionStart + UserPromptSubmit + PreToolUse Agent/Task); sub-agents use hook-injected context | +| Grok Build | `--grok` | `.grok/` | `.grok/skills/` | `.grok/agents/` | pull-based prelude (no hooks; flat `.grok/commands/trellis-*.md`) | +| Kimi Code | `--kimi` | `.kimi-code/` | `.agents/skills/` (shared) + `.kimi-code/skills/` | None — agent prompts are skills under `.kimi-code/skills/` and dispatch to the built-in `coder` | None (pull-based prelude; no project hooks/settings) | +| Snow CLI | `--snow` | `.snow/` | `.snow/skills/` | `.snow/agents/` (auto-discovered; primary path) | class-1: auto inject + project agents + `beforeSubAgentStart` (`.snow/hooks/` `session`/`user`/`subagent` modes -> `additionalContext` JSON); no legacy sub-agent JSON; commands `.snow/commands/trellis-*.json` | ## Capability Groups @@ -38,22 +44,36 @@ These platforms usually have `trellis-research`, `trellis-implement`, and `trell - GitHub Copilot - Factory Droid - Pi Agent +- Trae IDE +- Reasonix (delivered as skills with `runAs: subagent` under `.reasonix/skills/`, not as a separate `agents/` directory) +- ZCode +- Grok Build (`.grok/agents/`; dispatch via `spawn_subagent` with `subagent_type`) +- Kimi Code (delivered as skills under `.kimi-code/skills/`; dispatched to the built-in `coder`, including research because it must persist files) +- Snow CLI (`.snow/agents/`; auto-discovered project agents + class-1 hooks) When changing implementation/check/research behavior, look for the corresponding platform agent files first. +### Native Trellis Sub-Agent Tool + +Some platforms expose a first-class tool that the host runtime understands. The model calls it like any other tool and the host renders progress cards, validates the agent name against `.<platform>/agents/`, and enforces dispatch modes. + +- Pi Agent — `trellis_subagent` tool, defined in `.pi/extensions/trellis/index.ts`. Supports `single` / `parallel` / `chain` dispatch modes and emits live `trellis-subagent-progress` events. + +When changing sub-agent dispatch behavior on these platforms, edit the extension file, **not** the agent markdown — the agent markdown defines responsibilities, but the host extension owns dispatch, validation, and progress rendering. + ### Main-Session Workflow Platforms These platforms rely more on workflows/skills to guide the main session: - Kilo - Antigravity -- Windsurf +- Devin When changing behavior, inspect workflows and skills first. Do not assume Trellis sub-agents exist. ### Shared `.agents/skills/` -Codex writes the shared `.agents/skills/` layer. Some tools that support agentskills.io can also read this directory. If the user wants multiple compatible tools to share one skill, consider `.agents/skills/` first, but do not assume every platform reads it. +Codex, Gemini CLI, Pi Agent, and Kimi Code write the shared `.agents/skills/` layer. Some tools that support agentskills.io can also read this directory. If the user wants multiple compatible tools to share one skill, consider `.agents/skills/` first, but do not assume every platform reads it. ZCode keeps Trellis-managed skills under `.zcode/skills/`. ## Decision Rules When Modifying Platform Files @@ -72,3 +92,20 @@ Platform ecosystems change, and user projects may already be customized. If this - Judge behavior by the read rules currently written in the agent file. Do not delete a custom file just because it is not listed in this path table. + +### `.omp/` — Oh My Pi (OMP) + +Extension-backed platform. OMP native provider auto-discovers all subdirectories. + +```text +.omp/ +├── commands/ # Slash commands (flat .md) +├── skills/ # Auto-triggered skills (SKILL.md per dir) +├── agents/ # Agent definitions (.md) +└── extensions/ + └── trellis/ + └── index.ts # Trellis extension (context injection) +``` + +No `settings.json` — OMP scans `.omp/` subdirectories automatically. +No Python hooks — hook-equivalent behavior lives in the TypeScript extension. diff --git a/.agents/skills/trellis-meta/references/platform-files/skills-and-commands.md b/.agents/skills/trellis-meta/references/platform-files/skills-and-commands.md index 816c666a..89f15cec 100644 --- a/.agents/skills/trellis-meta/references/platform-files/skills-and-commands.md +++ b/.agents/skills/trellis-meta/references/platform-files/skills-and-commands.md @@ -25,12 +25,15 @@ Trellis workflow skills usually share one semantic set: brainstorm, before-dev, | Kiro | `.kiro/skills/` | | Gemini CLI | `.agents/skills/`, `.gemini/commands/` | | Antigravity | `.agent/skills/`, `.agent/workflows/` | -| Windsurf | `.windsurf/skills/`, `.windsurf/workflows/` | +| Devin | `.devin/skills/`, `.devin/workflows/` | | Qoder | `.qoder/skills/`, `.qoder/commands/` | | CodeBuddy | `.codebuddy/skills/`, `.codebuddy/commands/` | | GitHub Copilot | `.github/skills/`, `.github/prompts/` | | Factory Droid | `.factory/skills/`, `.factory/commands/` | -| Pi Agent | `.pi/skills/` | +| Pi Agent | `.agents/skills/` | +| Reasonix | `.reasonix/skills/` | +| ZCode | `.zcode/skills/`, `.zcode/commands/` | +| Kimi Code | `.agents/skills/`, `.kimi-code/skills/` (commands delivered as `/skill:trellis-*` skills) | In a user project, use the files actually generated by init as authoritative. diff --git a/.agents/skills/trellis-session-insight/SKILL.md b/.agents/skills/trellis-session-insight/SKILL.md new file mode 100644 index 00000000..1d9f4eda --- /dev/null +++ b/.agents/skills/trellis-session-insight/SKILL.md @@ -0,0 +1,81 @@ +--- +name: trellis-session-insight +description: "Reach into past AI conversation history through the `trellis mem` CLI. Use whenever the user asks 'how did we solve X last time', 'have we discussed this before', 'what was the decision on X', 'remind me what we did in this task', '上次怎么解的', '之前讨论过吗', '想起一段对话', or when starting a brainstorm that overlaps prior work, debugging a familiar bug, continuing a task across sessions, or doing a finish-work review. Returns raw past dialogue; decide for the moment whether to update spec, append to task notes, quote inline in the answer, or just internalize." +--- + +# Trellis Session Insight + +This skill teaches an AI **how to call `trellis mem`** — the project's cross-session memory feedstock — and **when reaching for it is the right move**. + +It is intentionally a **capability skill, not a workflow**. There is no fixed output file, no required write-back step, no "always run after finish-work" rule. What to do with what `mem` returns is a judgement call made in the moment of the conversation. The skill exists so the AI knows the capability is there and can decide. + +## What `trellis mem` is + +A local CLI that indexes the user's past Claude Code, Codex, Pi Agent, and ZCode conversation logs and lets you list, search, slice by Trellis task boundaries, and dump cleaned dialogue from them. Claude and Codex use `~/.claude/projects/` and `~/.codex/sessions/`. Pi uses its default or environment-configured session root, global `~/.pi/agent/settings.json`, and the scoped project's `.pi/settings.json`; relative `sessionDir` values resolve from the settings file directory. Project-local Pi settings require project-scoped lookup through the current cwd or `--cwd`. ZCode uses `~/.zcode/cli/db/db.sqlite`. OpenCode logs are not yet indexable (provider adapter pending) — when an OpenCode session is the obvious target, surface that limitation rather than guessing. + +Nothing in `mem` is uploaded. All reads are local. + +## When to reach for it + +The bar is "would a senior teammate ask 'didn't we already talk about this?'" — those are the moments. Some concrete patterns: + +- **Brainstorm rerun risk.** Starting a new task that touches an area the user has been in before, and you want to check whether a decision was already made — before re-asking the user. +- **Familiar-bug debugging.** The current bug pattern feels like one the user reported / fixed before. Pulling the relevant past session can save a full debugging loop. +- **Cross-session continuation.** The user resumes work after a gap and says "where were we" / "继续上次的" without being specific. +- **Decision retrieval.** The user references "the decision we made about X" but the decision lives in an old brainstorm, not in any `prd.md` / `spec/`. +- **Finish-work retrospective.** When the user explicitly asks for a wrap-up of what was decided / what hurt / what surprised them in this task — not as a forced step on every finish-work. +- **Pattern-spotting across past work.** The user asks "do I keep making the same mistake on X" / "我每次都踩这个坑吗" — search across sessions answers that. + +If none of these apply, don't call `mem`. It is a tool, not a ceremony. + +## When NOT to reach for it + +- The relevant context is already in the current turn, `prd.md`, `design.md`, recent `git log`, or the open files. `mem` is for stuff that has fallen out of immediate reach. +- The user is asking about a fact in the code, not a fact from a past conversation. `git log -p` / `grep` / reading the file directly is faster and more authoritative. +- You are in a sub-agent (`trellis-implement` / `trellis-check`) whose dispatch prompt already includes the curated `implement.jsonl` / `check.jsonl` context. Adding `mem` on top usually just clutters. +- The user has explicitly said "don't dig through history, just answer what I asked". + +## What to do with what `mem` returns + +Treat the output as **raw material**, not a deliverable. Once you have it, decide based on the live conversation: + +- **Quote inline in your reply** if a specific past exchange answers the user's current question — and cite the session-id / phase so the user can verify. +- **Update `<task>/prd.md` or `<task>/design.md`** if `mem` surfaced a load-bearing decision that should have been written down but wasn't. Surface the proposed edit to the user first. +- **Append to a task-local notes file** (e.g. `<task>/notes.md` or extending an existing one) if the finding belongs to the current task's record but doesn't fit the PRD. +- **Update `.trellis/spec/`** if the finding is a project-wide convention or gotcha that would help future tasks. Run the `trellis-update-spec` skill for that — `session-insight` ends at the discovery. +- **Just absorb it** for the next few turns and answer better, without writing anything. This is often the right move for one-off recall. + +Trellis does not prescribe a single destination. Forcing every recall into a fixed file makes the file grow into noise. Let the situation decide. + +## How to call it + +Full CLI reference is in `references/cli-quick-reference.md`. The 80% case is one of: + +```bash +# Find sessions whose contents mention a keyword (project-scope is default; +# add --global to search every project on this machine). +trellis mem search "<keyword>" + +# Dump dialogue from one session, optionally filtered by phase or keyword. +trellis mem extract <session-id> --phase brainstorm +trellis mem extract <session-id> --grep "<keyword>" + +# Drill into a session: top-N hit turns + surrounding context. +trellis mem context <session-id> --turns 3 --around 2 + +# When you do not know the session id yet, start with list + filter. +trellis mem list --cwd <project-path> +trellis mem projects # → list active project cwds, then narrow +``` + +Phase slicing (`--phase brainstorm|implement|all`) cuts the session at `task.py create` and `task.py start` boundaries. For a finish-work review of the current task, `--phase brainstorm` recovers the planning discussion and `--phase implement` recovers the execution loop. Default is `all`. + +## Triggering patterns + +`references/triggering-patterns.md` lists more verbatim user phrasings (English + Chinese) that should make you think "reach for `mem`" — keep that handy when training instinct. + +## Out of scope + +- `mem` does not edit code or update files. Any write-back is your decision in the moment. +- `mem` is read-only on the platform JSONL stores. It does not push or sync to remote. +- This skill does not replace `trellis-update-spec` (which is the right tool for promoting a finding into project-wide guidance) or the platform-native task / spec workflow. diff --git a/.agents/skills/trellis-session-insight/references/cli-quick-reference.md b/.agents/skills/trellis-session-insight/references/cli-quick-reference.md new file mode 100644 index 00000000..78540f25 --- /dev/null +++ b/.agents/skills/trellis-session-insight/references/cli-quick-reference.md @@ -0,0 +1,65 @@ +# `trellis mem` CLI Reference + +Full flag reference for the five subcommands. Pin this as the authoritative source — `trellis mem help` prints the same content at runtime, so anything here that drifts is a bug. + +## Subcommands + +| Command | Purpose | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `list` | List sessions. Default subcommand when none is given. | +| `search <keyword>` | Find sessions whose contents match a keyword. | +| `context <session-id>` | Drill into one session: top-N hit turns + surrounding context. Pair with `--grep` for keyword anchoring. | +| `extract <session-id>` | Dump cleaned dialogue. Combine with `--phase` / `--grep` to slice. | +| `projects` | List active project `cwd` values with session counts. Use this to discover which `--cwd` to pass to other subcommands. | + +## Flags (apply where meaningful) + +| Flag | Subcommands | Meaning | +| --------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--platform claude\|codex\|opencode\|pi\|all` | all | Default `all`. OpenCode adapter is currently a stub on `0.6.0-beta.*` — see "Caveats" below. | +| `--since YYYY-MM-DD` | list / search | Inclusive lower date bound. | +| `--until YYYY-MM-DD` | list / search | Inclusive upper date bound. | +| `--global` | list / search | Include sessions from every project on this machine. Default is the current project `cwd`. | +| `--cwd <path>` | list / search | Force a specific project cwd instead of inferring from where you are. | +| `--limit N` | list / search | Cap output rows. Default `50`. | +| `--grep KW` | extract / context | Filter turns by keyword. Multi-token AND when whitespace-separated. | +| `--phase brainstorm\|implement\|all` | extract | Slice session by Trellis task boundaries. `brainstorm` = `[task.py create, task.py start)`. `implement` = turns outside brainstorm windows. Default `all`. | +| `--turns N` | context | Number of hit turns to return. Default `3`. | +| `--around N` | context | Surrounding turns to include per hit. Default `1`. | +| `--max-chars N` | context | Total character budget. Default `6000` (~1500 tokens). | +| `--include-children` | search / context | Merge OpenCode sub-agent sessions into their parent session. | +| `--json` | all | Emit machine-parseable JSON instead of human-readable output. | + +## Common one-liners + +```bash +# What past sessions discussed "deadlock" anywhere on this machine? +trellis mem search "deadlock" --global --limit 20 + +# Inside a specific session, surface the top 5 turns that mention "lock contention" +# plus 2 turns of surrounding context. +trellis mem context 5842592d --grep "lock contention" --turns 5 --around 2 + +# Recover the brainstorm window for a session — useful when continuing a task +# the user started a week ago. +trellis mem extract 5842592d --phase brainstorm + +# List every project this machine has Trellis sessions for, with counts. +trellis mem projects +``` + +## Output shapes + +- **Default human output** (no `--json`): wrapped to a terminal, with session ids highlighted and turn markers visible. Suitable to read inline but messy to paste into a markdown file. +- **`--json`**: stable schema, safe to parse and process. When piping `mem` output into a follow-up step (e.g. summarizing for a Lessons section), prefer `--json`. + +## Caveats + +- **OpenCode adapter is a stub on `0.6.0-beta.*`.** When `--platform` resolves to OpenCode (or `all` and OpenCode would be included), `mem` prints a one-line "reader unavailable" notice and continues with the other platforms. Don't promise OpenCode coverage in your reply until the adapter ships. +- **`--phase` slicing depends on `task.py create` / `task.py start` invocations appearing in the recorded bash calls of the session.** Sessions where the user ran `task.py` from a different terminal — outside the recorded AI loop — will not have phase boundaries. `--phase all` is the safe fallback. +- **`mem` indexes platform JSONL files directly.** If the user has cleared their Claude / Codex / Pi session storage, `mem` cannot recover what is no longer on disk. +- **`mem` is read-only.** No remote sync, no edits to platform JSONL. Any write you do based on `mem` findings is your own follow-up call into the editing tools available to you. + +## When you need more than this reference + +Run `trellis mem help` in the user's shell. The runtime help is authoritative and will be ahead of this reference during fast-moving beta releases. diff --git a/.agents/skills/trellis-session-insight/references/triggering-patterns.md b/.agents/skills/trellis-session-insight/references/triggering-patterns.md new file mode 100644 index 00000000..66021ca5 --- /dev/null +++ b/.agents/skills/trellis-session-insight/references/triggering-patterns.md @@ -0,0 +1,93 @@ +# Triggering Patterns + +Verbatim user phrasings that should make an AI reach for `trellis mem`. Calibrate instinct against these — if a user message hits one of these patterns and you do not reach for `mem`, you probably missed an obvious recall. + +Patterns are grouped by the *intent* behind the phrasing, not the surface words. The same intent shows up in different languages and registers. + +## Past-solution recall + +The user is asking "how did we (or I) solve this before". Past dialogue holds the answer; the codebase shows the result but not the reasoning. + +- "How did we solve this last time?" +- "What did we end up doing about X?" +- "We dealt with this once already, didn't we?" +- "上次怎么解的?" +- "之前是怎么搞定 X 的?" +- "我记得以前修过类似的" + +Reach: `trellis mem search "<symptom keyword>" --global --limit 10`, then `context` into the hit that looks closest. + +## Decision retrieval + +The user is referencing a decision that lives in old dialogue, not in any committed file. Look in brainstorm windows. + +- "What was the decision on X?" +- "Did we decide to use Postgres or SQLite?" +- "The rationale for choosing X over Y was…?" +- "我们当时为啥选了 X 而不是 Y?" +- "关于 X 我们之前是怎么定的?" +- "之前讨论过 X 的方案吗?" + +Reach: `trellis mem search "<decision keyword>"` to find the session, then `extract <id> --phase brainstorm` to recover the discussion. + +## Cross-session continuation + +The user resumed work after a gap and the context is implicit. + +- "Where were we?" +- "Continue from last time." +- "Pick up where we left off." +- "继续上次的" +- "我们上次做到哪了" +- "接着昨天那个任务" + +Reach: `trellis mem list --task <current-task-dir>` to find the most recent sessions tied to the active task, then `extract` the last one. + +## Familiar-bug debugging + +The current bug feels like one already seen. Past sessions probably hold the resolution path. + +- "I feel like I've hit this before." +- "Doesn't this look like that bug from last month?" +- "Same kind of timeout I had in X." +- "这个错好像之前见过" +- "这个 bug 是不是上次那个?" +- "怎么又是这个 error?" + +Reach: `trellis mem search "<error message fragment>" --global`. Anchor on a short, distinctive token from the actual error string. + +## Self-pattern spotting + +The user is asking whether they keep repeating the same kind of mistake or decision. + +- "Do I always make this mistake?" +- "How often have I run into X?" +- "Is this a recurring thing for me?" +- "我每次都踩这个坑吗?" +- "我老犯这个错?" +- "这类问题之前出现过几次?" + +Reach: `trellis mem search "<topic>" --global --limit 50` and scan the dates / projects in the listing. Optionally `extract` two or three for comparison. + +## Finish-work retrospective (on demand) + +The user explicitly wants to look back at this task — not as a forced step, only when they ask. + +- "Summarize what we did in this task." +- "What were the key decisions / surprises?" +- "Write up the lessons from this round." +- "总结一下这次的经验" +- "记一下这次踩的坑" +- "复盘下这个任务" + +Reach: identify the current task's session id (from `.trellis/.runtime/sessions/*.json` or `mem list --task <task-dir>`), then `extract <id> --phase brainstorm` and `--phase implement`. Present a summary — surface concrete file:line citations where possible. Whether to also write the summary somewhere (PRD, spec, notes file) is the user's call; offer, don't auto-write. + +## Anti-patterns: do NOT reach for `mem` here + +- "What does this function do?" → read the file. +- "Why is this test failing?" → read the test output and the file. +- "What's the right pattern for X in our codebase?" → grep / read spec files. +- "What's the latest npm version of Y?" → call `npm view`. +- "Fix this bug." → debug. Reach for `mem` only if you suspect prior context exists; otherwise it is noise. + +The bar stays: would a senior teammate ask "didn't we already talk about this?" before answering? If yes, reach for `mem`. If no, don't. diff --git a/.agents/skills/trellis-spec-bootstarp/SKILL.md b/.agents/skills/trellis-spec-bootstrap/SKILL.md similarity index 97% rename from .agents/skills/trellis-spec-bootstarp/SKILL.md rename to .agents/skills/trellis-spec-bootstrap/SKILL.md index 2f7c7ad8..e1650df4 100644 --- a/.agents/skills/trellis-spec-bootstarp/SKILL.md +++ b/.agents/skills/trellis-spec-bootstrap/SKILL.md @@ -1,9 +1,9 @@ --- -name: trellis-spec-bootstarp +name: trellis-spec-bootstrap description: "Bootstrap project-specific Trellis coding specs with a platform-neutral single-agent workflow. Use when creating or refreshing .trellis/spec guidelines, analyzing a codebase with GitNexus, ABCoder, or source inspection, decomposing package/layer spec work, and writing real codebase-backed spec docs without placeholder text." --- -# Trellis Spec Bootstarp +# Trellis Spec Bootstrap Use this skill to create or refresh `.trellis/spec/` guidelines from the real codebase. One capable agent owns the full loop: analyze the repository, choose the spec boundaries, write the docs, and verify the result. The workflow does not depend on a specific host, CLI, or agent brand. diff --git a/.agents/skills/trellis-spec-bootstarp/references/mcp-setup.md b/.agents/skills/trellis-spec-bootstrap/references/mcp-setup.md similarity index 100% rename from .agents/skills/trellis-spec-bootstarp/references/mcp-setup.md rename to .agents/skills/trellis-spec-bootstrap/references/mcp-setup.md diff --git a/.agents/skills/trellis-spec-bootstarp/references/repository-analysis.md b/.agents/skills/trellis-spec-bootstrap/references/repository-analysis.md similarity index 100% rename from .agents/skills/trellis-spec-bootstarp/references/repository-analysis.md rename to .agents/skills/trellis-spec-bootstrap/references/repository-analysis.md diff --git a/.agents/skills/trellis-spec-bootstarp/references/spec-task-planning.md b/.agents/skills/trellis-spec-bootstrap/references/spec-task-planning.md similarity index 100% rename from .agents/skills/trellis-spec-bootstarp/references/spec-task-planning.md rename to .agents/skills/trellis-spec-bootstrap/references/spec-task-planning.md diff --git a/.agents/skills/trellis-spec-bootstarp/references/spec-writing.md b/.agents/skills/trellis-spec-bootstrap/references/spec-writing.md similarity index 100% rename from .agents/skills/trellis-spec-bootstarp/references/spec-writing.md rename to .agents/skills/trellis-spec-bootstrap/references/spec-writing.md diff --git a/.agents/skills/trellis-start/SKILL.md b/.agents/skills/trellis-start/SKILL.md index a20d447d..3c7980d2 100644 --- a/.agents/skills/trellis-start/SKILL.md +++ b/.agents/skills/trellis-start/SKILL.md @@ -5,7 +5,7 @@ description: "Initializes an AI development session by reading workflow guides, # Start Session -Initialize a Trellis-managed development session. This platform has no session-start hook, so manually load the equivalent context by following these steps (each one mirrors a section the hook would otherwise inject). +Initialize a Trellis-managed development session. This platform has no session-start hook, so manually load the equivalent compact context by following these steps. --- @@ -19,7 +19,7 @@ python3 ./.trellis/scripts/get_context.py If this output includes a line beginning `Trellis update available:`, copy the full line verbatim when summarizing session context. Do not shorten operational command hints. ## Step 2: Workflow overview -Phase Index + skill routing table + DO-NOT-skip rules. +Compact Phase Index, request triage rules, planning artifact contract, and the step-detail command. ```bash python3 ./.trellis/scripts/get_context.py --mode phase @@ -39,14 +39,15 @@ cat .trellis/spec/<package>/<layer>/index.md # for each relevant layer Index files list the specific guideline docs to read when you actually start coding. ## Step 4: Decide next action -From Step 1 you know the current task. Check the task directory: +From Step 1 you know the current task and status. Check the task directory: -- **Active task + `prd.md` exists** → Phase 2 step 2.1. Load the step detail: +- **Active task status `planning` + no `prd.md`** → Phase 1.1. Load the `trellis-brainstorm` skill. +- **Active task status `planning` + `prd.md` exists** → stay in Phase 1. Lightweight tasks can be PRD-only; complex tasks need `design.md` + `implement.md`. Load the relevant Phase 1 step detail before `task.py start`. +- **Active task status `in_progress`** → Phase 2 step 2.1. Load the step detail: ```bash python3 ./.trellis/scripts/get_context.py --mode phase --step 2.1 --platform codex ``` -- **Active task + no `prd.md`** → Phase 1.1. Load the `trellis-brainstorm` skill. -- **No active task** → when the user describes multi-step work, load the `trellis-brainstorm` skill to clarify requirements, then create a task via `task.py create`. For simple one-off questions or trivial edits, skip this and just answer directly — no task needed. +- **No active task** → classify first. For simple conversation / small task, ask only whether this turn should create a Trellis task. For complex work, ask whether you may create a Trellis task and enter planning. If the user says no, skip Trellis for this session. --- diff --git a/.codex/agents/trellis-check.toml b/.codex/agents/trellis-check.toml index 98965199..157e6ed9 100644 --- a/.codex/agents/trellis-check.toml +++ b/.codex/agents/trellis-check.toml @@ -1,33 +1,10 @@ name = "trellis-check" description = "Workspace-write Trellis reviewer that self-fixes spec drift, lint/type-check failures, and missing tests." sandbox_mode = "workspace-write" +# model = "gpt-5.6-terra" +# model_reasoning_effort = "high" developer_instructions = """ -## Required: Load Trellis Context First - -This platform does NOT auto-inject task context via hook. Before doing anything else, you MUST load context yourself. - -### Step 1: Find the active task path - -Try in order — stop at the first one that yields a task path: - -1. **Look at the dispatch prompt** you received from the main agent. If its first line is `Active task: <path>` (e.g. `Active task: .trellis/tasks/04-17-foo`), use that path. The main agent is required to include this line on class-2 platforms. -2. **Run** `python3 ./.trellis/scripts/task.py current --source` and read the `Current task:` line. -3. **If both fail** (no `Active task:` line in the prompt and `task.py current` returns no task), ask the user which task to work on; do NOT guess. - -### Step 2: Load task context from the resolved path - -1. Read the task's `prd.md` (requirements) and `info.md` if it exists (technical design). -2. Read `<task-path>/check.jsonl` — JSONL list of dev spec files relevant to this agent. -3. For each entry in the JSONL, Read its `file` path — these are the dev specs you must follow. - **Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran). - -If `check.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read `prd.md`, list available specs with `python3 ./.trellis/scripts/get_context.py --mode packages`, and pick the specs that match the task domain yourself. Do NOT block on the missing jsonl — proceed with prd-only context plus your spec judgment. - -If the resolved task path has no `prd.md`, ask the user what to work on; do NOT proceed without context. - ---- - You are running as the `trellis-check` sub-agent. The main session has dispatched you to review and self-fix. CRITICAL — Recursion guard (read first): @@ -37,33 +14,13 @@ CRITICAL — Recursion guard (read first): --- -## Required: Load Trellis Context First - -This platform does NOT auto-inject task context via hook. Before doing anything else, you MUST load context yourself. - -### Step 1: Find the active task path - -Try in order — stop at the first one that yields a task path: - -1. **Look at the dispatch prompt** you received from the main agent. If its first line is `Active task: <path>` (e.g. `Active task: .trellis/tasks/04-17-foo`), use that path. The main agent is required to include this line on class-2 platforms. -2. **Run** `python3 ./.trellis/scripts/task.py current --source` and read the `Current task:` line. -3. **If both fail** (no `Active task:` line in the prompt and `task.py current` returns no task), ask the user which task to work on; do NOT guess. - -### Step 2: Load task context from the resolved path - -1. Read the task's `prd.md` (requirements) and `info.md` if it exists (technical design). -2. Read `<task-path>/check.jsonl` — JSONL list of dev spec files relevant to this agent. -3. For each entry in the JSONL, Read its `file` path — these are the dev specs you must follow. - **Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran). - -If `check.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read `prd.md`, list available specs with `python3 ./.trellis/scripts/get_context.py --mode packages`, and pick the specs that match the task domain yourself. Do NOT block on the missing jsonl — proceed with prd-only context plus your spec judgment. - -If the resolved task path has no `prd.md`, ask the user what to work on; do NOT proceed without context. - ---- - You are the Trellis reviewer agent. +Trellis Context Loading Protocol: +- Look for the `<!-- trellis-hook-injected -->` marker in your input above. +- If the marker is present, the native `SubagentStart` hook has already loaded the role-specific task artifacts and spec context. Proceed directly with the check work. +- If the marker is absent, find `Active task: <path>` in your dispatch prompt. Read `<path>/check.jsonl`, each file listed there, `<path>/prd.md`, `<path>/design.md` if present, and `<path>/implement.md` if present before checking. If the dispatch prompt has no active-task path, ask the main session; do not guess or use another session's task. + Your job is to review code changes against specs AND fix issues directly — not just report them. You have write access; use it. Review checklist: @@ -95,15 +52,3 @@ Only list issues you could not self-fix (e.g. missing product decision, out-of-s If no issues are found, say so explicitly after verifying lint/type-check pass. """ - -# Disable Codex collab tools entirely for this sub-agent. With both -# multi_agent and multi_agent_v2 off, `spawn_agent` / `wait_agent` / -# `list_agents` / `close_agent` are not registered in the sub-agent's tool -# list at all — the model literally cannot call them. This is the structural -# fix for the wait_agent self-deadlock when the parent inherits its -# transcript via Codex's default `fork_turns="all"` (#240 follow-up, #241). -[features] -multi_agent = false - -[features.multi_agent_v2] -enabled = false diff --git a/.codex/agents/trellis-implement.toml b/.codex/agents/trellis-implement.toml index 765bb948..e25f4c56 100644 --- a/.codex/agents/trellis-implement.toml +++ b/.codex/agents/trellis-implement.toml @@ -1,33 +1,10 @@ name = "trellis-implement" description = "Workspace-write Trellis implementer that follows specs and keeps generated templates in sync." sandbox_mode = "workspace-write" +# model = "gpt-5.6-terra" +# model_reasoning_effort = "high" developer_instructions = """ -## Required: Load Trellis Context First - -This platform does NOT auto-inject task context via hook. Before doing anything else, you MUST load context yourself. - -### Step 1: Find the active task path - -Try in order — stop at the first one that yields a task path: - -1. **Look at the dispatch prompt** you received from the main agent. If its first line is `Active task: <path>` (e.g. `Active task: .trellis/tasks/04-17-foo`), use that path. The main agent is required to include this line on class-2 platforms. -2. **Run** `python3 ./.trellis/scripts/task.py current --source` and read the `Current task:` line. -3. **If both fail** (no `Active task:` line in the prompt and `task.py current` returns no task), ask the user which task to work on; do NOT guess. - -### Step 2: Load task context from the resolved path - -1. Read the task's `prd.md` (requirements) and `info.md` if it exists (technical design). -2. Read `<task-path>/implement.jsonl` — JSONL list of dev spec files relevant to this agent. -3. For each entry in the JSONL, Read its `file` path — these are the dev specs you must follow. - **Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran). - -If `implement.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read `prd.md`, list available specs with `python3 ./.trellis/scripts/get_context.py --mode packages`, and pick the specs that match the task domain yourself. Do NOT block on the missing jsonl — proceed with prd-only context plus your spec judgment. - -If the resolved task path has no `prd.md`, ask the user what to work on; do NOT proceed without context. - ---- - You are running as the `trellis-implement` sub-agent. The main session has dispatched you to do the work. CRITICAL — Recursion guard (read first): @@ -37,33 +14,13 @@ CRITICAL — Recursion guard (read first): --- -## Required: Load Trellis Context First - -This platform does NOT auto-inject task context via hook. Before doing anything else, you MUST load context yourself. - -### Step 1: Find the active task path - -Try in order — stop at the first one that yields a task path: - -1. **Look at the dispatch prompt** you received from the main agent. If its first line is `Active task: <path>` (e.g. `Active task: .trellis/tasks/04-17-foo`), use that path. The main agent is required to include this line on class-2 platforms. -2. **Run** `python3 ./.trellis/scripts/task.py current --source` and read the `Current task:` line. -3. **If both fail** (no `Active task:` line in the prompt and `task.py current` returns no task), ask the user which task to work on; do NOT guess. - -### Step 2: Load task context from the resolved path - -1. Read the task's `prd.md` (requirements) and `info.md` if it exists (technical design). -2. Read `<task-path>/implement.jsonl` — JSONL list of dev spec files relevant to this agent. -3. For each entry in the JSONL, Read its `file` path — these are the dev specs you must follow. - **Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran). - -If `implement.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read `prd.md`, list available specs with `python3 ./.trellis/scripts/get_context.py --mode packages`, and pick the specs that match the task domain yourself. Do NOT block on the missing jsonl — proceed with prd-only context plus your spec judgment. - -If the resolved task path has no `prd.md`, ask the user what to work on; do NOT proceed without context. - ---- - You are the Trellis implementer agent. +Trellis Context Loading Protocol: +- Look for the `<!-- trellis-hook-injected -->` marker in your input above. +- If the marker is present, the native `SubagentStart` hook has already loaded the role-specific task artifacts and spec context. Proceed directly with the implementation work. +- If the marker is absent, find `Active task: <path>` in your dispatch prompt. Read `<path>/implement.jsonl`, each file listed there, `<path>/prd.md`, `<path>/design.md` if present, and `<path>/implement.md` if present before doing the work. If the dispatch prompt has no active-task path, ask the main session; do not guess or use another session's task. + Rules: - Read before write. Follow `.trellis/spec/` guidance relevant to the task. - Keep changes focused on the requested scope. @@ -76,15 +33,3 @@ Before finishing, summarize: - Tests/checks run - Remaining risks or follow-ups """ - -# Disable Codex collab tools entirely for this sub-agent. With both -# multi_agent and multi_agent_v2 off, `spawn_agent` / `wait_agent` / -# `list_agents` / `close_agent` are not registered in the sub-agent's tool -# list at all — the model literally cannot call them. This is the structural -# fix for the wait_agent self-deadlock when the parent inherits its -# transcript via Codex's default `fork_turns="all"` (#240 follow-up, #241). -[features] -multi_agent = false - -[features.multi_agent_v2] -enabled = false diff --git a/.codex/agents/trellis-research.toml b/.codex/agents/trellis-research.toml index 1e0efeb4..cba82b8d 100644 --- a/.codex/agents/trellis-research.toml +++ b/.codex/agents/trellis-research.toml @@ -1,6 +1,8 @@ name = "trellis-research" description = "Trellis researcher for specs, code patterns, and affected files. Writes findings into {TASK_DIR}/research/ — read-only elsewhere." sandbox_mode = "workspace-write" +# model = "gpt-5.6-terra" +# model_reasoning_effort = "high" developer_instructions = """ You are the Trellis researcher agent. @@ -11,11 +13,21 @@ Conversations get compacted; files don't. Every research topic MUST be persisted to `{TASK_DIR}/research/<topic>.md`. Returning findings only through the chat reply is a failure. +## Trellis Context Loading Protocol + +Look for the `<!-- trellis-hook-injected -->` marker in your input above. + +- If the marker is present, the native `SubagentStart` hook has supplied the + `Active task: <path>` header and research-only context. Use that task path. +- If the marker is absent, find `Active task: <path>` in your dispatch prompt. + If it is absent too, ask the main session for the path; do not run + `task.py current`, guess, or use another session's task. +- Do not load `implement.jsonl` or `check.jsonl`; research is role-isolated. + ## Workflow -1. Run `python3 ./.trellis/scripts/task.py current --source` to get the - active task path and source. If no active task is set, ask the user - where to write output; do not guess. +1. Use the active task path supplied by the context-loading protocol. If no + path is supplied, ask the user where to write output; do not guess. 2. Run `mkdir -p <TASK_DIR>/research` to ensure the directory exists. 3. Read `.trellis/workflow.md`, relevant `.trellis/spec/` files, and target code before forming an opinion. @@ -59,15 +71,3 @@ If the user asks you to edit code, decline and tell them to spawn the ... ``` """ - -# Disable Codex collab tools entirely for this sub-agent. With both -# multi_agent and multi_agent_v2 off, `spawn_agent` / `wait_agent` / -# `list_agents` / `close_agent` are not registered in the sub-agent's tool -# list at all — the model literally cannot call them. This is the structural -# fix for the wait_agent self-deadlock when the parent inherits its -# transcript via Codex's default `fork_turns="all"` (#240 follow-up, #241). -[features] -multi_agent = false - -[features.multi_agent_v2] -enabled = false diff --git a/.codex/config.toml b/.codex/config.toml index 39ada5b6..e1feee37 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -26,3 +26,14 @@ project_doc_fallback_filenames = ["AGENTS.md"] # aborts the entire config load and blocks Codex from starting. Codex's own # default for multi_agent_v2 is used instead; tune it in your user-level # config if needed. + +# Pin the subagent recursion depth explicitly instead of relying on Codex's +# default. #445 removed the per-agent `[features] multi_agent = false` guard +# (the #240/#241 wait_agent-deadlock structural fix) because native subagent +# dispatch already caps recursion via `agents.max_depth` — but that key is +# global/user-level, not settable inside an individual agent's .toml. Pinning +# it here means an upstream default change, or a user's own global override, +# can't silently reopen the recursion the #240/#241 fix closed. Project config +# (this file) takes precedence over user-level `~/.codex/config.toml`. +[agents] +max_depth = 1 diff --git a/.codex/hooks.json b/.codex/hooks.json index 78b6bc89..dc04480a 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -10,6 +10,18 @@ } ] } + ], + "SubagentStart": [ + { + "matcher": "^(?:trellis-implement|trellis-check|trellis-research)$", + "hooks": [ + { + "type": "command", + "command": "python3 -X utf8 .codex/hooks/inject-subagent-context.py", + "timeout": 15 + } + ] + } ] } } diff --git a/.codex/hooks/inject-subagent-context.py b/.codex/hooks/inject-subagent-context.py new file mode 100755 index 00000000..35590ea1 --- /dev/null +++ b/.codex/hooks/inject-subagent-context.py @@ -0,0 +1,1133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Multi-Platform Sub-Agent Context Injection Hook + +Injects task-specific context when sub-agents (implement, check, research) are spawned. + +Core Design Philosophy: +- Hook is responsible for injecting all context, subagent works autonomously with complete info +- Each agent has a dedicated jsonl file defining its context +- No resume needed, no segmentation, behavior controlled by code not prompt + +Trigger: PreToolUse (before Task tool call) + +Context Source: Trellis active task resolver points to task directory +- implement.jsonl - Implement agent dedicated context +- check.jsonl - Check agent dedicated context +- prd.md - Requirements document +- design.md - Technical design for complex tasks +- implement.md - Execution plan for complex tasks +- codex-review-output.txt - Code Review results +""" +from __future__ import annotations + +# IMPORTANT: Suppress all warnings FIRST +import warnings +warnings.filterwarnings("ignore") + +import json +import os +import sys +from pathlib import Path +from typing import Any + +# IMPORTANT: Force stdout to use UTF-8 on Windows +# This fixes UnicodeEncodeError when outputting non-ASCII characters +if sys.platform.startswith("win"): + import io as _io + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] + elif hasattr(sys.stdout, "detach"): + sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr] + + +# ============================================================================= +# Path Constants (change here to rename directories) +# ============================================================================= + +DIR_WORKFLOW = ".trellis" +DIR_SPEC = "spec" +FILE_TASK_JSON = "task.json" + +# ============================================================================= +# Subagent Constants (change here to rename subagent types) +# ============================================================================= + +AGENT_IMPLEMENT = "trellis-implement" +AGENT_CHECK = "trellis-check" +AGENT_RESEARCH = "trellis-research" + +# Agents that require a task directory +AGENTS_REQUIRE_TASK = (AGENT_IMPLEMENT, AGENT_CHECK) +# All supported agents +AGENTS_ALL = (AGENT_IMPLEMENT, AGENT_CHECK, AGENT_RESEARCH) + + +def find_repo_root(start_path: str) -> str | None: + """ + Find git repo root from start_path upwards + + Returns: + Repo root path, or None if not found + """ + current = Path(start_path).resolve() + while current != current.parent: + if (current / ".git").exists(): + return str(current) + current = current.parent + return None + + +def _detect_platform(input_data: dict) -> str | None: + if _hook_event_name(input_data) == "SubagentStart": + return "codex" + if isinstance(input_data.get("cursor_version"), str): + return "cursor" + env_map = { + "ZCODE_PROJECT_DIR": "zcode", + "CLAUDE_PROJECT_DIR": "claude", + "CURSOR_PROJECT_DIR": "cursor", + "CODEBUDDY_PROJECT_DIR": "codebuddy", + "FACTORY_PROJECT_DIR": "droid", + "GEMINI_PROJECT_DIR": "gemini", + "QODER_PROJECT_DIR": "qoder", + "KIRO_PROJECT_DIR": "kiro", + "COPILOT_PROJECT_DIR": "copilot", + } + for env_name, platform in env_map.items(): + if os.environ.get(env_name): + return platform + script_parts = set(Path(sys.argv[0]).parts) + if ".claude" in script_parts: + return "claude" + if ".cursor" in script_parts: + return "cursor" + if ".gemini" in script_parts: + return "gemini" + if ".qoder" in script_parts: + return "qoder" + if ".codebuddy" in script_parts: + return "codebuddy" + if ".factory" in script_parts: + return "droid" + if ".kiro" in script_parts: + return "kiro" + if ".zcode" in script_parts: + return "zcode" + return None + + +def get_current_task( + repo_root: str, + input_data: dict, + *, + platform: str | None = None, + allow_single_session_fallback: bool = True, + allow_environment_context: bool = True, + require_existing: bool = False, +) -> str | None: + """Resolve current task directory through the unified active task resolver.""" + scripts_dir = Path(repo_root) / DIR_WORKFLOW / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + try: + from common.active_task import resolve_active_task # type: ignore[import-not-found] + except Exception: + return None + + active = resolve_active_task( + Path(repo_root), + input_data, + platform=platform or _detect_platform(input_data), + allow_single_session_fallback=allow_single_session_fallback, + allow_environment_context=allow_environment_context, + ) + if require_existing and active.stale: + return None + return active.task_path + + +# ============================================================================= +# Context Injection Limits (issue #441) +# +# Notice text and behavior mirrored byte-for-byte in the Pi TS extension +# (templates/pi/extensions/trellis/index.ts.txt). Changing wording here +# requires changing it there too. +# ============================================================================= + +DEFAULT_MAX_FILE_BYTES = 32768 +DEFAULT_MAX_ARTIFACT_BYTES = 65536 +DEFAULT_MAX_TOTAL_BYTES = 131072 + +DEFAULT_LIMITS: dict[str, int] = { + "max_file_bytes": DEFAULT_MAX_FILE_BYTES, + "max_artifact_bytes": DEFAULT_MAX_ARTIFACT_BYTES, + "max_total_bytes": DEFAULT_MAX_TOTAL_BYTES, +} + + +def _get_limits(repo_root: str) -> dict[str, int]: + """Load context-injection byte limits from config.yaml, with safe fallback.""" + scripts_dir = Path(repo_root) / DIR_WORKFLOW / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + try: + from common.config import get_context_injection_limits # type: ignore[import-not-found] + + return get_context_injection_limits(Path(repo_root)) + except Exception: + return dict(DEFAULT_LIMITS) + + +def truncate_utf8(data: bytes, cap: int) -> bytes: + """Truncate ``data`` to at most ``cap`` bytes without splitting a UTF-8 + multi-byte sequence. + + ``cap <= 0`` means "no limit" — returns ``data`` unchanged. + """ + if cap <= 0 or len(data) <= cap: + return data + + truncated = data[:cap] + i = len(truncated) + # Back off over continuation bytes (10xxxxxx) to find the lead byte. + while i > 0 and (truncated[i - 1] & 0xC0) == 0x80: + i -= 1 + if i == 0: + return b"" + + lead = truncated[i - 1] + if lead & 0x80: + if (lead & 0xE0) == 0xC0: + seq_len = 2 + elif (lead & 0xF0) == 0xE0: + seq_len = 3 + elif (lead & 0xF8) == 0xF0: + seq_len = 4 + else: + seq_len = 1 + # Drop the lead byte too if its full sequence didn't fit. + if (i - 1) + seq_len > len(truncated): + i -= 1 + + return truncated[:i] + + +class _Budget: + """Tracks the running total of bytes emitted into the sub-agent context.""" + + def __init__(self, max_total_bytes: int) -> None: + self.max_total_bytes = max_total_bytes + self.used = 0 + + def has_room(self, size: int) -> bool: + if self.max_total_bytes <= 0: + return True + return self.used + size <= self.max_total_bytes + + def add(self, size: int) -> None: + self.used += size + + +def _read_file_bytes(base_path: str, file_path: str) -> bytes | None: + """Read raw file bytes, return None if file doesn't exist.""" + full_path = os.path.join(base_path, file_path) + if os.path.exists(full_path) and os.path.isfile(full_path): + try: + with open(full_path, "rb") as f: + return f.read() + except Exception: + return None + return None + + +def _truncate_notice(path: str, cap: int) -> str: + return f"\n[Trellis: truncated at {cap} bytes — read {path} for the full content]" + + +def _is_binary_content(data: bytes) -> bool: + """Return True when raw bytes should not be decoded into model context.""" + if b"\x00" in data: + return True + try: + data.decode("utf-8", errors="strict") + except UnicodeDecodeError: + return True + return False + + +def _binary_notice(path: str, size: int, reason: str) -> str: + return ( + f"[Trellis: not inlined (binary file) — " + f"{path} ({size} bytes): {reason}]" + ) + + +def _index_notice(path: str, size: int, reason: str) -> str: + return ( + f"[Trellis: not inlined (total context limit reached) — " + f"{path} ({size} bytes): {reason}]" + ) + + +def _budgeted_block( + budget: _Budget, + header: str, + plain_path: str, + content: str, + reason: str, + size_for_index: int, +) -> str: + """Return an inlined ``=== header ===`` block, or degrade to an index + notice once the total context budget is exhausted.""" + block = f"=== {header} ===\n{content}" + block_bytes = len(block.encode("utf-8")) + if not budget.has_room(block_bytes): + notice = _index_notice(plain_path, size_for_index, reason) + budget.add(len(notice.encode("utf-8"))) + return notice + budget.add(block_bytes) + return block + + +def _materialize_file( + base_path: str, + file_path: str, + reason: str, + limits: dict[str, int], + budget: _Budget, +) -> str | None: + """Read a JSONL-referenced file, apply the per-file cap, then budget it.""" + data = _read_file_bytes(base_path, file_path) + if data is None: + return None + + size = len(data) + if _is_binary_content(data): + notice = _binary_notice(file_path, size, reason) + budget.add(len(notice.encode("utf-8"))) + return notice + + cap = limits["max_file_bytes"] + truncated_bytes = truncate_utf8(data, cap) + content = truncated_bytes.decode("utf-8", errors="replace") + if len(truncated_bytes) < size: + content += _truncate_notice(file_path, cap) + + return _budgeted_block(budget, file_path, file_path, content, reason, size) + + +def _materialize_directory( + base_path: str, + dir_path: str, + reason: str, + limits: dict[str, int], + budget: _Budget, + max_files: int = 20, +) -> list[str]: + """Read all .md files in a directory, applying the same per-file and + total caps as a single-file JSONL entry.""" + full_path = os.path.join(base_path, dir_path) + if not os.path.exists(full_path) or not os.path.isdir(full_path): + return [] + + blocks: list[str] = [] + try: + md_files = sorted( + f + for f in os.listdir(full_path) + if f.endswith(".md") and os.path.isfile(os.path.join(full_path, f)) + ) + for filename in md_files[:max_files]: + relative_path = os.path.join(dir_path, filename) + block = _materialize_file(base_path, relative_path, reason, limits, budget) + if block: + blocks.append(block) + except Exception: + pass + + return blocks + + +def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[dict]: + """ + Parse all file/directory entries referenced in a jsonl context file. + + Schema: + {"file": "path/to/file.md", "reason": "..."} + {"file": "path/to/dir/", "type": "directory", "reason": "..."} + {"_example": "..."} # seed row — skipped (no `file` field) + + Rows without a ``file`` field (e.g. the self-describing seed line written + by ``task.py create`` before the agent has curated entries) are skipped + silently. If the resulting entry list is empty, a stderr warning is + emitted so the operator can debug missing context. + + Returns: + [{"file": path, "type": "file" | "directory", "reason": reason}, ...] + """ + full_path = os.path.join(base_path, jsonl_path) + if not os.path.exists(full_path): + print( + f"[inject-subagent-context] WARN: {jsonl_path} not found — " + f"sub-agent will receive only task artifacts", + file=sys.stderr, + ) + return [] + + entries: list[dict] = [] + saw_real_entry = False + try: + with open(full_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + item = json.loads(line) + file_path = item.get("file") or item.get("path") + + if not file_path: + # Seed / comment row — skip silently + continue + + saw_real_entry = True + entries.append( + { + "file": file_path, + "type": item.get("type", "file"), + "reason": item.get("reason") or "-", + } + ) + except json.JSONDecodeError: + continue + except Exception: + pass + + if not saw_real_entry: + print( + f"[inject-subagent-context] WARN: {jsonl_path} has no curated " + f"entries (only seed / empty) — sub-agent will receive only " + f"task artifacts. See workflow.md planning artifact guidance.", + file=sys.stderr, + ) + + return entries + + +def _materialize_jsonl_entries( + base_path: str, jsonl_path: str, limits: dict[str, int], budget: _Budget +) -> list[str]: + """Materialize every entry in a jsonl context file into context blocks, + applying per-file and total budget caps.""" + blocks: list[str] = [] + for entry in read_jsonl_entries(base_path, jsonl_path): + if entry["type"] == "directory": + blocks.extend( + _materialize_directory( + base_path, entry["file"], entry["reason"], limits, budget + ) + ) + else: + block = _materialize_file( + base_path, entry["file"], entry["reason"], limits, budget + ) + if block: + blocks.append(block) + return blocks + + +def get_agent_context( + repo_root: str, + task_dir: str, + agent_type: str, + limits: dict[str, int], + budget: _Budget, +) -> str: + """ + Get context from {agent_type}.jsonl for the specified agent. + Only reads implement.jsonl or check.jsonl (the two JSONL files the task system creates). + """ + agent_jsonl = f"{task_dir}/{agent_type}.jsonl" + blocks = _materialize_jsonl_entries(repo_root, agent_jsonl, limits, budget) + return "\n\n".join(blocks) + + +def _materialize_artifact( + base_path: str, + file_path: str, + header_label: str, + reason: str, + limits: dict[str, int], + budget: _Budget, +) -> str | None: + """Read a task artifact (prd/design/implement.md), apply the per-artifact + cap, then budget it.""" + data = _read_file_bytes(base_path, file_path) + if data is None: + return None + + size = len(data) + cap = limits["max_artifact_bytes"] + truncated_bytes = truncate_utf8(data, cap) + content = truncated_bytes.decode("utf-8", errors="replace") + if len(truncated_bytes) < size: + content += _truncate_notice(file_path, cap) + + return _budgeted_block(budget, header_label, file_path, content, reason, size) + + +def get_implement_context(repo_root: str, task_dir: str) -> str: + """ + Complete context for Implement Agent + + Read order: + 1. All files in implement.jsonl (spec/research manifests) + 2. prd.md (requirements) + 3. design.md if present (technical design) + 4. implement.md if present (execution plan) + """ + limits = _get_limits(repo_root) + budget = _Budget(limits["max_total_bytes"]) + context_parts = [] + + # 1. Read implement.jsonl + base_context = get_agent_context(repo_root, task_dir, "implement", limits, budget) + if base_context: + context_parts.append(base_context) + + # 2. Requirements document + prd_block = _materialize_artifact( + repo_root, + f"{task_dir}/prd.md", + f"{task_dir}/prd.md (Requirements)", + "Requirements document", + limits, + budget, + ) + if prd_block: + context_parts.append(prd_block) + + # 3. Technical design for complex tasks + design_block = _materialize_artifact( + repo_root, + f"{task_dir}/design.md", + f"{task_dir}/design.md (Technical Design)", + "Technical design document", + limits, + budget, + ) + if design_block: + context_parts.append(design_block) + + # 4. Execution plan for complex tasks + implement_plan_block = _materialize_artifact( + repo_root, + f"{task_dir}/implement.md", + f"{task_dir}/implement.md (Execution Plan)", + "Execution plan document", + limits, + budget, + ) + if implement_plan_block: + context_parts.append(implement_plan_block) + + return "\n\n".join(context_parts) + + +def get_check_context(repo_root: str, task_dir: str) -> str: + """ + Context for Check Agent: check.jsonl + task artifacts. + """ + limits = _get_limits(repo_root) + budget = _Budget(limits["max_total_bytes"]) + context_parts = [] + + base_context = get_agent_context(repo_root, task_dir, "check", limits, budget) + if base_context: + context_parts.append(base_context) + + prd_block = _materialize_artifact( + repo_root, + f"{task_dir}/prd.md", + f"{task_dir}/prd.md (Requirements)", + "Requirements document", + limits, + budget, + ) + if prd_block: + context_parts.append(prd_block) + + design_block = _materialize_artifact( + repo_root, + f"{task_dir}/design.md", + f"{task_dir}/design.md (Technical Design)", + "Technical design document", + limits, + budget, + ) + if design_block: + context_parts.append(design_block) + + implement_plan_block = _materialize_artifact( + repo_root, + f"{task_dir}/implement.md", + f"{task_dir}/implement.md (Execution Plan)", + "Execution plan document", + limits, + budget, + ) + if implement_plan_block: + context_parts.append(implement_plan_block) + + return "\n\n".join(context_parts) + + +def get_finish_context(repo_root: str, task_dir: str) -> str: + """ + Context for Finish phase: reuses check.jsonl + prd.md + (Finish is a final check, same context source.) + """ + return get_check_context(repo_root, task_dir) + + + +def build_implement_prompt(original_prompt: str, context: str) -> str: + """Build complete prompt for Implement""" + return f"""<!-- trellis-hook-injected --> +# Implement Agent Task + +You are the Implement Agent in the Multi-Agent Pipeline. + +## Your Context + +All the information you need has been prepared for you: + +{context} + +--- + +## Your Task + +{original_prompt} + +--- + +## Workflow + +1. **Understand specs** - All dev specs are injected above, understand them + 2. **Understand task artifacts** - Read requirements, technical design if present, and execution plan if present + 3. **Implement feature** - Implement following specs and task artifacts +4. **Self-check** - Ensure code quality against check specs + +## Important Constraints + +- Do NOT execute git commit, only code modifications +- Follow all dev specs injected above +- Report list of modified/created files when done""" + + +def build_check_prompt(original_prompt: str, context: str) -> str: + """Build complete prompt for Check""" + return f"""<!-- trellis-hook-injected --> +# Check Agent Task + +You are the Check Agent in the Multi-Agent Pipeline (code and cross-layer checker). + +## Your Context + +All check specs and dev specs you need: + +{context} + +--- + +## Your Task + +{original_prompt} + +--- + +## Workflow + +1. **Get changes** - Run `git diff --name-only` and `git diff` to get code changes +2. **Check against specs** - Check item by item against specs above +3. **Self-fix** - Fix issues directly, don't just report +4. **Run verification** - Run project's lint and typecheck commands + +## Important Constraints + +- Fix issues yourself, don't just report +- Must execute complete checklist in check specs +- Pay special attention to impact radius analysis (L1-L5)""" + + +def build_finish_prompt(original_prompt: str, context: str) -> str: + """Build complete prompt for Finish (final check before PR)""" + return f"""<!-- trellis-hook-injected --> +# Finish Agent Task + +You are performing the final check before creating a PR. + +## Your Context + +Finish checklist and requirements: + +{context} + +--- + +## Your Task + +{original_prompt} + +--- + +## Workflow + +1. **Review changes** - Run `git diff --name-only` to see all changed files + 2. **Verify task artifacts** - Check requirements in prd.md and, when present, design.md / implement.md +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 +- Verify design.md and implement.md constraints when those files are present""" + + + +def get_research_context(repo_root: str, task_dir: str | None) -> str: + """ + Context for Research Agent — project structure overview for spec directories. + + `task_dir` kept for signature parity with get_implement_context / get_check_context + so the dispatcher can call them uniformly. + """ + _ = task_dir + context_parts = [] + + # 1. Project structure overview (dynamically discover spec directories) + spec_path = f"{DIR_WORKFLOW}/{DIR_SPEC}" + spec_root = Path(repo_root) / DIR_WORKFLOW / DIR_SPEC + + # Build spec tree dynamically + tree_lines = [f"{spec_path}/"] + if spec_root.is_dir(): + pkg_dirs = sorted(d for d in spec_root.iterdir() if d.is_dir()) + for i, pkg_dir in enumerate(pkg_dirs): + is_last = i == len(pkg_dirs) - 1 + prefix = "└── " if is_last else "├── " + layers = sorted(d.name for d in pkg_dir.iterdir() if d.is_dir()) + layer_info = f" ({', '.join(layers)})" if layers else "" + tree_lines.append(f"{prefix}{pkg_dir.name}/{layer_info}") + + spec_tree = "\n".join(tree_lines) + + project_structure = f"""## Project Spec Directory Structure + +``` +{spec_tree} +``` + +To get structured package info, run: `python3 ./{DIR_WORKFLOW}/scripts/get_context.py --mode packages` + +## Search Tips + +- Spec files: `{spec_path}/**/*.md` +- Code search: Use Glob and Grep tools +- Tech solutions: Use mcp__exa__web_search_exa or mcp__exa__get_code_context_exa""" + + context_parts.append(project_structure) + + return "\n\n".join(context_parts) + + +def build_research_prompt(original_prompt: str, context: str) -> str: + """Build complete prompt for Research""" + return f"""# Research Agent Task + +You are the Research Agent in the Multi-Agent Pipeline (search researcher). + +## Core Principle + +**You do one thing: find and explain information.** + +You are a documenter, not a reviewer. + +## Project Info + +{context} + +--- + +## Your Task + +{original_prompt} + +--- + +## Workflow + +1. **Understand query** - Determine search type (internal/external) and scope +2. **Plan search** - List search steps for complex queries +3. **Execute search** - Execute multiple independent searches in parallel +4. **Organize results** - Output structured report + +## Search Tools + +| Tool | Purpose | +|------|---------| +| Glob | Search by filename pattern | +| Grep | Search by content | +| Read | Read file content | +| mcp__exa__web_search_exa | External web search | +| mcp__exa__get_code_context_exa | External code/doc search | + +## Strict Boundaries + +**Only allowed**: Describe what exists, where it is, how it works + +**Forbidden** (unless explicitly asked): +- Suggest improvements +- Criticize implementation +- Recommend refactoring +- Modify any files + +## Report Format + +Provide structured search results including: +- List of files found (with paths) +- Code pattern analysis (if applicable) +- Related spec documents +- External references (if any)""" + + +def _string_value(value: Any) -> str: + if isinstance(value, str): + stripped = value.strip() + return stripped + return "" + + +def _hook_event_name(input_data: dict) -> str: + """Return a hook event name from the documented snake/camel-case fields.""" + return _string_value( + input_data.get("hook_event_name") or input_data.get("hookEventName") + ) + + +def _codex_subagent_type(input_data: dict) -> str: + """Return a Trellis Codex agent type only for a native start event.""" + if _hook_event_name(input_data) != "SubagentStart": + return "" + agent_type = _string_value( + input_data.get("agent_type") or input_data.get("agentType") + ) + return agent_type if agent_type in AGENTS_ALL else "" + + +def build_codex_subagent_context( + subagent_type: str, + task_dir: str, + context: str, +) -> str: + """Build developer context for a native, already-dispatched Codex role.""" + role = subagent_type.removeprefix("trellis-") + return f"""<!-- trellis-hook-injected --> +# Trellis Native {role.title()} Subagent + +You are the dispatched `{subagent_type}` role for this task. Perform that role +directly; do not follow main-session dispatch or wait instructions, and do not +spawn another Trellis subagent. + +Active task: {task_dir} + +## Curated Context + +{context}""" + + +def _handle_codex_subagent_start(input_data: dict) -> None: + """Emit Codex developer context for a recognised native Trellis subagent. + + The event supplies the parent session id. Disabling the generic + single-session fallback is essential here: native starts must never borrow + a task from another Codex window when that parent id is absent or stale. + """ + subagent_type = _codex_subagent_type(input_data) + parent_session_id = _string_value(input_data.get("session_id")) + if not subagent_type or not parent_session_id: + return + + cwd = _string_value(input_data.get("cwd")) or os.getcwd() + repo_root = find_repo_root(cwd) + if not repo_root: + return + + task_dir = get_current_task( + repo_root, + {"session_id": parent_session_id}, + platform="codex", + allow_single_session_fallback=False, + allow_environment_context=False, + require_existing=True, + ) + if not task_dir: + return + + if subagent_type in AGENTS_REQUIRE_TASK: + task_dir_full = Path(repo_root) / task_dir + if not task_dir_full.is_dir(): + return + + if subagent_type == AGENT_IMPLEMENT: + context = get_implement_context(repo_root, task_dir) + elif subagent_type == AGENT_CHECK: + context = get_check_context(repo_root, task_dir) + else: + context = get_research_context(repo_root, task_dir) + + if not context: + return + + output = { + "hookSpecificOutput": { + "hookEventName": "SubagentStart", + "additionalContext": build_codex_subagent_context( + subagent_type, task_dir, context + ), + } + } + print(json.dumps(output, ensure_ascii=False)) + + +def _extract_subagent_name(value: Any) -> str: + """Extract a sub-agent name from common platform encodings. + + Cursor's native Task args encode custom sub-agents as a protobuf oneof, + which can appear in hook JSON as either ``{"custom": {"name": "..."}}`` + or ``{"type": {"case": "custom", "value": {"name": "..."}}}``. + """ + direct = _string_value(value) + if direct: + return direct + + if not isinstance(value, dict): + return "" + + for key in ("name", "subagent_type_name", "subagentTypeName"): + direct = _string_value(value.get(key)) + if direct: + return direct + + custom = value.get("custom") + if isinstance(custom, dict): + custom_name = _string_value(custom.get("name")) + if custom_name: + return custom_name + + oneof = value.get("type") + if isinstance(oneof, dict): + case_name = _string_value(oneof.get("case")) + if case_name == "custom": + nested_value = oneof.get("value") + if isinstance(nested_value, dict): + custom_name = _string_value(nested_value.get("name")) + if custom_name: + return custom_name + if case_name: + return case_name + + case_name = _string_value(value.get("case")) + if case_name == "custom": + nested_value = value.get("value") + if isinstance(nested_value, dict): + custom_name = _string_value(nested_value.get("name")) + if custom_name: + return custom_name + if case_name: + return case_name + + for agent_name in AGENTS_ALL: + if agent_name in value: + return agent_name + + return "" + + +def _extract_subagent_type(tool_input: dict) -> str: + for key in ( + "subagent_type", + "subagentType", + "subagent_type_name", + "subagentTypeName", + "agent_type", + "agentType", + "name", + ): + agent_name = _extract_subagent_name(tool_input.get(key)) + if agent_name: + return agent_name + return "" + + +def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]: + """Parse hook input across different platform formats. + + Returns (subagent_type, original_prompt, tool_input). + Handles: + - Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type + - Cursor: tool_name=Task|Subagent, tool_input.subagent_type + - Copilot CLI: toolName=task (camelCase key, lowercase value) + - ZCode: toolName=Agent, toolInput/tool_input.subagent_type + - Gemini CLI: tool_name IS the agent name (BeforeTool matcher already filtered) + - Kiro: agentSpawn hook, agent_name field at top level + """ + tool_input = input_data.get("tool_input", {}) + if not isinstance(tool_input, dict): + tool_input = input_data.get("toolInput", {}) + if not isinstance(tool_input, dict): + tool_input = {} + + # Standard format: Task/Agent tool with subagent_type + tool_name = input_data.get("tool_name", "") or input_data.get("toolName", "") + if tool_name.lower() in ("task", "agent", "subagent"): + return ( + _extract_subagent_type(tool_input), + tool_input.get("prompt", ""), + tool_input, + ) + + # Kiro: agentSpawn hook passes agent_name at top level + agent_name = input_data.get("agent_name", "") + if agent_name: + return agent_name, tool_input.get("prompt", input_data.get("prompt", "")), tool_input + + # Gemini CLI: BeforeTool where tool_name IS the agent name + # (matcher already ensured it's one of our agents) + if tool_name in AGENTS_ALL: + return tool_name, tool_input.get("prompt", ""), tool_input + + # Copilot CLI: toolName field (camelCase), value might be the agent name + tool_name_camel = input_data.get("toolName", "") + if tool_name_camel in AGENTS_ALL: + return tool_name_camel, input_data.get("toolArgs", ""), tool_input + + return "", "", tool_input + + +def main(): + if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": + sys.exit(0) + + try: + input_data = json.load(sys.stdin) + except json.JSONDecodeError: + sys.exit(0) + if not isinstance(input_data, dict): + sys.exit(0) + + if _hook_event_name(input_data) == "SubagentStart": + try: + _handle_codex_subagent_start(input_data) + except Exception: + # A native context hook must never prevent Codex from spawning the + # requested child when its runtime state is unavailable or stale. + pass + sys.exit(0) + + subagent_type, original_prompt, tool_input = _parse_hook_input(input_data) + cwd = input_data.get("cwd", os.getcwd()) + + # Only handle subagent types we care about + if subagent_type not in AGENTS_ALL: + sys.exit(0) + + # Find repo root + repo_root = find_repo_root(cwd) + if not repo_root: + sys.exit(0) + + # Get current task directory (research doesn't require it) + task_dir = get_current_task(repo_root, input_data) + + # implement/check need task directory + if subagent_type in AGENTS_REQUIRE_TASK: + if not task_dir: + sys.exit(0) + # Check if task directory exists + task_dir_full = os.path.join(repo_root, task_dir) + if not os.path.exists(task_dir_full): + sys.exit(0) + + # Check for [finish] marker in prompt (check agent with finish context) + is_finish_phase = "[finish]" in original_prompt.lower() + + # Get context and build prompt based on subagent type + if subagent_type == AGENT_IMPLEMENT: + assert task_dir is not None # validated above + context = get_implement_context(repo_root, task_dir) + new_prompt = build_implement_prompt(original_prompt, context) + elif subagent_type == AGENT_CHECK: + assert task_dir is not None # validated above + if is_finish_phase: + # Finish phase: use finish context (lighter, focused on final verification) + context = get_finish_context(repo_root, task_dir) + new_prompt = build_finish_prompt(original_prompt, context) + else: + # Regular check phase: use check context (full specs for self-fix loop) + context = get_check_context(repo_root, task_dir) + new_prompt = build_check_prompt(original_prompt, context) + elif subagent_type == AGENT_RESEARCH: + # Research can work without task directory + context = get_research_context(repo_root, task_dir) + new_prompt = build_research_prompt(original_prompt, context) + else: + sys.exit(0) + + if not context: + sys.exit(0) + + # Return updated input. Most platforms ignore unrecognized fields, so we + # include multiple formats. ZCode is stricter; live probing confirmed the + # nested Claude-compatible shape below reaches the sub-agent prompt. + updated = {**tool_input, "prompt": new_prompt} + if _detect_platform(input_data) == "zcode": + output = { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow", + "updatedInput": updated, + } + } + else: + output = { + # Claude Code / Qoder / CodeBuddy / Droid format + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow", + "updatedInput": updated, + }, + # Cursor format + "permission": "allow", + "updated_input": updated, + # Gemini format + "updatedInput": updated, + } + + print(json.dumps(output, ensure_ascii=False)) + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/.codex/hooks/inject-workflow-state.py b/.codex/hooks/inject-workflow-state.py old mode 100644 new mode 100755 index bd2dca6f..ab8e276c --- a/.codex/hooks/inject-workflow-state.py +++ b/.codex/hooks/inject-workflow-state.py @@ -18,9 +18,11 @@ missing or a tag is absent, the breadcrumb degrades to a generic the broken state instead of the hook silently masking it. Shared across all hook-capable platforms (Claude, Cursor, Codex, Qoder, -CodeBuddy, Droid, Gemini, Copilot). Kiro is not wired (no per-turn -hook entry point). Written to each platform's hooks directory via -writeSharedHooks() at init time. +CodeBuddy, Droid, Gemini, Copilot, Kiro). Kiro wires this via the CLI +custom agent's ``hooks.userPromptSubmit`` and the IDE ``.kiro.hook`` +``promptSubmit`` event; its output branch emits a plain-text breadcrumb +(Kiro adds hook stdout directly to the conversation context). Written to +each platform's hooks directory via writeSharedHooks() at init time. Silent exit 0 cases (no output): - No .trellis/ directory found (not a Trellis project) @@ -32,6 +34,8 @@ import json import os import re import sys +import queue +import threading from pathlib import Path # Force UTF-8 on stdin/stdout/stderr on Windows. Default codepage there is @@ -49,53 +53,20 @@ if sys.platform.startswith("win"): try: _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] except Exception: - pass + pass # Optional Windows stream setup; keep hook startup non-fatal. elif hasattr(_stream, "detach"): try: setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) except Exception: - pass + pass # Optional Windows stream setup; keep hook startup non-fatal. from typing import Optional -CODEX_SUB_AGENT_NOTICE = """<sub-agent-notice> -SUB-AGENT NOTICE - READ FIRST IF SPAWNED VIA spawn_agent - -If your parent session spawned you via spawn_agent with an explicit task -message above this hook output, that message is your only job. -- Execute the parent message exactly as written, then return. -- Ignore all Trellis workflow guidance below this notice. -- Do NOT call task.py start, task.py add-context, or task.py archive. -- Do NOT call wait_agent or spawn_agent. -- Do NOT modify .trellis/tasks/* or any other file unless the parent message - explicitly asks for that. - -If you are the main interactive Codex session and the user is typing at the -terminal with no parent agent, use the workflow guidance below normally. -</sub-agent-notice>""" - - -# Bootstrap notice for Codex while the session has no active task. Replaces the -# heavyweight SessionStart context injection — instead of pushing 9.5 KB of -# workflow text up front, we just nudge the AI to read the `trellis-start` skill once. -# The nudge keeps showing up while status == "no_task" (cheap text, AI won't -# re-read after the first time). Once a task is created the breadcrumb status -# flips and this notice stops appearing automatically. Sub-agents are warded -# off by the <sub-agent-notice> above plus the explicit exemption below. +# Bootstrap notice for Codex while the session has no active task. Codex does not +# get the full SessionStart overview; this short reminder points the main session +# at the start skill once and leaves the per-turn state block compact. CODEX_NO_TASK_BOOTSTRAP_NOTICE = """<trellis-bootstrap> -You are running in a Trellis-managed Codex session and there is no active task yet. -If you have not already loaded Trellis context this session, read the `trellis-start` skill once: - - $trellis-start - -(equivalent to reading `.agents/skills/trellis-start/SKILL.md` and following its Steps 1-3) - -The skill walks you through workflow.md, dev profile, git status, active tasks, and spec -indexes. Then route the user's request per the <workflow-state> A/B/C rules below. - -Sub-agent exemption: if you are a sub-agent (spawned via spawn_agent with a parent task -message), DO NOT read `$trellis-start`. Execute the parent message directly as instructed by the -<sub-agent-notice> above. +If you have not already loaded Trellis context this session, read the `trellis-start` skill once. </trellis-bootstrap>""" @@ -125,6 +96,9 @@ def _detect_platform(input_data: dict) -> str | None: if isinstance(input_data.get("cursor_version"), str): return "cursor" env_map = { + # ZCode may set both ZCODE_PROJECT_DIR and CLAUDE_PROJECT_DIR; check + # ZCODE first so ZCode sessions aren't misdetected as claude. + "ZCODE_PROJECT_DIR": "zcode", "CLAUDE_PROJECT_DIR": "claude", "CURSOR_PROJECT_DIR": "cursor", "CODEBUDDY_PROJECT_DIR": "codebuddy", @@ -133,6 +107,7 @@ def _detect_platform(input_data: dict) -> str | None: "QODER_PROJECT_DIR": "qoder", "KIRO_PROJECT_DIR": "kiro", "COPILOT_PROJECT_DIR": "copilot", + "TRAE_PROJECT_DIR": "trae", } for env_name, platform in env_map.items(): if os.environ.get(env_name): @@ -154,6 +129,10 @@ def _detect_platform(input_data: dict) -> str | None: return "droid" if ".kiro" in script_parts: return "kiro" + if ".trae" in script_parts: + return "trae" + if ".zcode" in script_parts: + return "zcode" return None @@ -249,25 +228,89 @@ def _read_trellis_config(root: Path) -> dict: return {} +DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD = "no-trellis" + + +def _resolve_skip_keyword(config: dict) -> str: + """Read `prompt_injection.skip_keyword` from parsed .trellis/config.yaml. + + Mirrors `common.config.get_prompt_injection_config()`. Defaults to + "no-trellis"; "" disables the escape hatch entirely. A non-string value + falls back to the default. + """ + if isinstance(config, dict): + section = config.get("prompt_injection") + if isinstance(section, dict): + raw = section.get("skip_keyword", DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD) + if isinstance(raw, str): + return raw + return DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD + + +def prompt_has_skip_keyword(prompt: str, keyword: str) -> bool: + """Case-insensitive, word-boundary match of `keyword` in `prompt`. + + Hyphen counts as a word char so "no-trellisx" / "xno-trellis" / + "foo-no-trellis" don't match, but punctuation/whitespace boundaries do. + Empty keyword never matches (disables the escape hatch). + """ + if not keyword or not isinstance(prompt, str): + return False + pattern = r"(?<![\w-])" + re.escape(keyword) + r"(?![\w-])" + return re.search(pattern, prompt, re.IGNORECASE) is not None + + +def _resolve_codex_dispatch_mode(config: dict) -> str: + """Normalize `codex.dispatch_mode` from .trellis/config.yaml to "auto" or "inline". + + Defaults to `auto`. The legacy `sub-agent` value is an alias for `auto`. + Any other explicit value (including invalid ones) falls back to `inline` + without per-turn warnings. Shared by `_codex_mode_banner` (the per-turn + banner) and `resolve_breadcrumb_key` (the breadcrumb tag key) so the two + stay in lockstep. + """ + mode = "auto" + if isinstance(config, dict): + codex_cfg = config.get("codex") + if isinstance(codex_cfg, dict): + cfg_mode = str(codex_cfg.get("dispatch_mode", mode)).strip().lower() + if cfg_mode == "inline": + mode = "inline" + elif cfg_mode in ("auto", "sub-agent"): + mode = "auto" + else: + mode = "inline" + return mode + + def _codex_mode_banner(config: dict) -> str: """Emit a `<codex-mode>` banner for the additionalContext payload. Reads `codex.dispatch_mode` from .trellis/config.yaml; defaults to - `inline` when missing or invalid because Codex sub-agents run with - `fork_turns="none"` isolation and can't inherit the parent session's - task context. The banner makes the active mode explicit to Codex AI - per turn, complementing the workflow-state body which is per-status. - Mode tells AI which dispatch protocol to follow; workflow-state tells - AI what step it's at. + `auto`, which dispatches Trellis sub-agents using native Codex context + injection with a child-side fallback. This does not rely on inherited + parent transcripts: `fork_turns` remains caller-controlled, and + fresh-history sub-agents still receive their explicit delegated task and + inherited session configuration. `inline` is an explicit opt-out; the + legacy `sub-agent` value is an alias for `auto`. Invalid explicit values + fall back to `inline` without per-turn warnings. The banner makes the + active mode explicit to Codex AI per turn, complementing the workflow-state + body which is per-status. Mode tells AI which dispatch protocol to follow; + workflow-state tells AI what step it's at. """ - mode = "inline" - if isinstance(config, dict): - codex_cfg = config.get("codex") - if isinstance(codex_cfg, dict): - cfg_mode = codex_cfg.get("dispatch_mode") - if cfg_mode in ("inline", "sub-agent"): - mode = cfg_mode - return f"<codex-mode>{mode}</codex-mode>" + mode = _resolve_codex_dispatch_mode(config) + if mode == "auto": + meaning = ( + "auto: implement/check work defaults to Trellis sub-agents; native Codex " + "context injection is preferred and child-side loading is the fallback. " + "The main session still coordinates, clarifies, updates specs, commits, and finishes." + ) + else: + meaning = ( + "inline: the main session implements/checks directly; " + "do not dispatch implement/check sub-agents." + ) + return f"<codex-mode>{meaning}</codex-mode>" def resolve_breadcrumb_key( @@ -275,22 +318,17 @@ def resolve_breadcrumb_key( ) -> str: """Pick the breadcrumb tag key based on Codex dispatch_mode. - Codex defaults to ``inline`` because sub-agents run with ``fork_turns="none"`` - isolation and can't inherit the parent session's task context. Users can - opt into ``codex.dispatch_mode: sub-agent`` in ``.trellis/config.yaml`` - to use the parallel ``<status>-inline`` tag → ``<status>`` flip. Invalid - or missing values fall back to inline. + Codex defaults to ``auto`` and therefore uses the ordinary ``<status>`` + breadcrumb for native SubagentStart dispatch with child-side fallback; + it does not depend on an inherited parent transcript. ``inline`` selects + the parallel ``<status>-inline`` tag; ``sub-agent`` remains an alias for + ``auto``. Invalid explicit values fall back to inline without per-turn + warnings. Non-codex platforms return the plain status unchanged. """ if platform == "codex": - mode = "inline" - if isinstance(config, dict): - codex_cfg = config.get("codex") - if isinstance(codex_cfg, dict): - cfg_mode = codex_cfg.get("dispatch_mode") - if cfg_mode in ("inline", "sub-agent"): - mode = cfg_mode + mode = _resolve_codex_dispatch_mode(config) return f"{status}-inline" if mode == "inline" else status return status @@ -316,8 +354,6 @@ def build_breadcrumb( if body is None: body = "Refer to workflow.md for current step." header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})" - if source: - header = f"{header}\nSource: {source}" return f"<workflow-state>\n{header}\n{body}\n</workflow-state>" @@ -325,14 +361,44 @@ def build_breadcrumb( # Entry # --------------------------------------------------------------------------- +def _load_hook_input() -> dict: + """Read hook JSON without trusting host runners to close stdin. + + Kiro IDE `runCommand` and similar hook runners can leave stdin open while + sending no payload. A plain `json.load(sys.stdin)` then blocks forever. + Normal hook runners write the complete JSON payload and close stdin, so the + short daemon read preserves that path while failing closed to `{}` for + non-piping hosts. + """ + result_queue: "queue.Queue[str | Exception]" = queue.Queue(maxsize=1) + + def _read() -> None: + try: + result_queue.put(sys.stdin.read()) + except Exception as exc: + result_queue.put(exc) + + reader = threading.Thread(target=_read, daemon=True) + reader.start() + try: + raw = result_queue.get(timeout=0.2) + except queue.Empty: + return {} + + if isinstance(raw, Exception): + return {} + try: + data = json.loads(raw) if raw.strip() else {} + except (json.JSONDecodeError, ValueError): + return {} + return data if isinstance(data, dict) else {} + + def main() -> int: if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": return 0 - try: - data = json.load(sys.stdin) - except (json.JSONDecodeError, ValueError): - data = {} + data = _load_hook_input() cwd_str = data.get("cwd") or os.getcwd() cwd = Path(cwd_str) @@ -341,9 +407,12 @@ def main() -> int: if root is None: return 0 # not a Trellis project + config = _read_trellis_config(root) + if prompt_has_skip_keyword(data.get("prompt", ""), _resolve_skip_keyword(config)): + return 0 # user opted out of the per-turn breadcrumb for this turn + templates = load_breadcrumbs(root) platform = _detect_platform(data) - config = _read_trellis_config(root) task = get_active_task(root, data) if task is None: # No active task — still emit a breadcrumb nudging AI toward @@ -355,17 +424,26 @@ def main() -> int: else: task_id, status, source = task status_key = resolve_breadcrumb_key(status, platform, config) + source_for_breadcrumb = None if platform == "codex" else source breadcrumb = build_breadcrumb( - task_id, status, templates, source, breadcrumb_key=status_key + task_id, status, templates, source_for_breadcrumb, breadcrumb_key=status_key ) if platform == "codex": - parts: list[str] = [CODEX_SUB_AGENT_NOTICE] + parts: list[str] = [] if task is None: parts.append(CODEX_NO_TASK_BOOTSTRAP_NOTICE) parts.append(_codex_mode_banner(config)) parts.append(breadcrumb) breadcrumb = "\n\n".join(parts) + # Kiro (CLI userPromptSubmit / IDE promptSubmit) adds a hook's stdout + # directly to the conversation context — no JSON envelope. Emit the bare + # breadcrumb text. Conditionally isolated: all other platforms keep the + # hookSpecificOutput JSON path below unchanged. + if platform == "kiro": + print(breadcrumb) + return 0 + # Gemini CLI 0.40.x rejects "UserPromptSubmit" — its per-turn event is # named "BeforeAgent". Other platforms (Claude/Cursor/Qoder/CodeBuddy/ # Droid/Codex/Copilot) accept the original Claude-style name. diff --git a/.codex/hooks/session-start.py b/.codex/hooks/session-start.py old mode 100644 new mode 100755 index 2e176d1a..ca5608f3 --- a/.codex/hooks/session-start.py +++ b/.codex/hooks/session-start.py @@ -33,12 +33,12 @@ if sys.platform.startswith("win"): try: _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] except Exception: - pass + pass # Optional Windows stream setup; keep hook startup non-fatal. elif hasattr(_stream, "detach"): try: setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) except Exception: - pass + pass # Optional Windows stream setup; keep hook startup non-fatal. def _normalize_windows_shell_path(path_str: str) -> str: @@ -92,27 +92,16 @@ def _normalize_windows_shell_path(path_str: str) -> str: warnings.filterwarnings("ignore") FIRST_REPLY_NOTICE = """<first-reply-notice> -On the first visible assistant reply in this session, begin with exactly one short Chinese sentence: -Trellis SessionStart 已注入:workflow、当前任务状态、开发者身份、git 状态、active tasks、spec 索引已加载。 -Then continue directly with the user's request. This notice is one-shot: do not repeat it after the first assistant reply in the same session. +On the first visible assistant reply in this session, briefly acknowledge that Trellis SessionStart context loaded. +Choose the acknowledgment language in this order: +1. Use the language of the user's current request (the user message that triggered this reply). +2. If that request has no clear natural language, use an explicitly established project communication language. +3. If neither provides a language, output the language-neutral fallback exactly: `Trellis SessionStart ✓`. +Continue directly with the user's request after the acknowledgment. +The acknowledgment must not alter the language used for the remainder of the response. +This notice is one-shot: do not repeat it after the first visible assistant reply in this session. </first-reply-notice>""" -SUB_AGENT_NOTICE = """<sub-agent-notice> -SUB-AGENT NOTICE - READ FIRST IF SPAWNED VIA spawn_agent - -If your parent session spawned you via spawn_agent with an explicit task -message above this hook output, that message is your only job. -- Execute the parent message exactly as written, then return. -- Ignore all Trellis workflow guidance below this notice. -- Do NOT call task.py start, task.py add-context, or task.py archive. -- Do NOT call wait_agent or spawn_agent. -- Do NOT modify .trellis/tasks/* or any other file unless the parent message - explicitly asks for that. - -If you are the main interactive Codex session and the user is typing at the -terminal with no parent agent, use the workflow guidance below normally. -</sub-agent-notice>""" - def should_skip_injection() -> bool: if os.environ.get("TRELLIS_HOOKS") == "0": @@ -133,7 +122,7 @@ def configure_project_encoding(project_dir: Path) -> None: configure_encoding() except Exception: - pass + pass # Optional encoding helper; host defaults are still usable. def _has_curated_jsonl_entry(jsonl_path: Path) -> bool: @@ -240,12 +229,19 @@ def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path: def _get_task_status(trellis_dir: Path, hook_input: dict) -> str: active = _resolve_active_task(trellis_dir, hook_input) if not active.task_path: - return f"Status: NO ACTIVE TASK\nSource: {active.source}\nNext: Describe what you want to work on" + return ( + "Status: NO ACTIVE TASK\n" + "Next: Classify the current turn and ask for task-creation consent " + "before creating any Trellis task." + ) task_ref = active.task_path task_dir = _resolve_task_dir(trellis_dir, task_ref) if active.stale or not task_dir.is_dir(): - return f"Status: STALE POINTER\nTask: {task_ref}\nSource: {active.source}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish" + return ( + f"Status: STALE POINTER\nTask: {task_ref}\n" + "Next: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish" + ) task_json_path = task_dir / "task.json" task_data: dict = {} @@ -253,46 +249,176 @@ def _get_task_status(trellis_dir: Path, hook_input: dict) -> str: try: task_data = json.loads(task_json_path.read_text(encoding="utf-8")) except (json.JSONDecodeError, PermissionError): - pass + pass # Optional task metadata; fall back to generic status. task_title = task_data.get("title", task_ref) task_status = task_data.get("status", "unknown") if task_status == "completed": - return f"Status: COMPLETED\nTask: {task_title}\nSource: {active.source}\nNext: Archive with `python3 ./.trellis/scripts/task.py archive {task_dir.name}` or start a new task" - - has_context = False - for jsonl_name in ("implement.jsonl", "check.jsonl", "spec.jsonl"): - jsonl_path = task_dir / jsonl_name - if jsonl_path.is_file() and _has_curated_jsonl_entry(jsonl_path): - has_context = True - break + return ( + f"Status: COMPLETED\nTask: {task_title}\n" + f"Next: Archive with `python3 ./.trellis/scripts/task.py archive {task_dir.name}` " + "or start a new task." + ) has_prd = (task_dir / "prd.md").is_file() + has_design = (task_dir / "design.md").is_file() + has_implement = (task_dir / "implement.md").is_file() + present = [ + name + for name in ("prd.md", "design.md", "implement.md", "implement.jsonl", "check.jsonl") + if (task_dir / name).is_file() + ] + present_line = ", ".join(present) if present else "none" if not has_prd: - return f"Status: NOT READY\nTask: {task_title}\nSource: {active.source}\nMissing: prd.md not created\nNext: Write PRD (see workflow.md Phase 1.1) then curate implement.jsonl per Phase 1.3" + return ( + f"Status: PLANNING\nTask: {task_title}\nPresent: {present_line}\n" + "Next: Load trellis-brainstorm and write prd.md. Stay in planning." + ) - if not has_context: - return f"Status: NOT READY\nTask: {task_title}\nSource: {active.source}\nMissing: implement.jsonl / check.jsonl missing or empty\nNext: Curate entries per workflow.md Phase 1.3 (spec + research files only), then `task.py start`" + if task_status == "planning": + if has_design and has_implement: + next_action = "Review planning artifacts with the user before `task.py start`." + else: + next_action = ( + "Lightweight task can ask for start review with PRD-only; " + "complex task must add design.md and implement.md before `task.py start`." + ) + return ( + f"Status: PLANNING\nTask: {task_title}\nPresent: {present_line}\n" + f"Next: {next_action}" + ) return ( - f"Status: READY\nTask: {task_title}\n" - f"Source: {active.source}\n" - "Next required action: dispatch `trellis-implement` per Phase 2.1. " - "For agent-capable platforms, the default is to NOT edit code in the main session. " - "After implementation, dispatch `trellis-check` per Phase 2.2 before reporting completion.\n" - "Sub-agent self-exemption: if you are reading this as a `trellis-implement` or " - "`trellis-check` sub-agent (your own role / agent name reflects that), this dispatch " - "instruction does NOT apply to you — you are already the dispatched sub-agent. " - "Implement / check directly without spawning another sub-agent of the same kind.\n" - "User override (per-turn escape hatch): if the user's CURRENT message explicitly tells the " - "main session to handle it directly (\"你直接改\" / \"别派 sub-agent\" / \"main session 写就行\" / " - "\"do it inline\" / \"不用 sub-agent\"), honor it for this turn and edit code directly. " - "Per-turn only; do NOT invent an override the user did not say." + f"Status: {task_status.upper()}\nTask: {task_title}\nPresent: {present_line}\n" + "Next: Follow the matching per-turn workflow-state. Context order is jsonl entries, " + "prd.md, design.md if present, implement.md if present." ) +def _run_git(repo_root: Path, args: list[str]) -> str: + try: + result = subprocess.run( + ["git", *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=3, + cwd=str(repo_root), + ) + except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError): + return "" + if result.returncode != 0: + return "" + return result.stdout.strip() + + +def _format_git_state(repo_root: Path) -> str: + branch = _run_git(repo_root, ["branch", "--show-current"]) or "(detached)" + dirty_lines = [ + line for line in _run_git(repo_root, ["status", "--porcelain"]).splitlines() + if line.strip() + ] + dirty_text = "clean" if not dirty_lines else f"dirty {len(dirty_lines)} paths" + return f"Git: branch {branch}; {dirty_text}." + + +def _repo_relative(repo_root: Path, path: Path) -> str: + try: + return path.relative_to(repo_root).as_posix() + except ValueError: + return str(path) + + +def _collect_spec_index_paths(trellis_dir: Path) -> list[str]: + paths: list[str] = [] + guides_index = trellis_dir / "spec" / "guides" / "index.md" + if guides_index.is_file(): + paths.append(".trellis/spec/guides/index.md") + + spec_dir = trellis_dir / "spec" + if not spec_dir.is_dir(): + return paths + + for sub in sorted(spec_dir.iterdir()): + if not sub.is_dir() or sub.name.startswith(".") or sub.name == "guides": + continue + index_file = sub / "index.md" + if index_file.is_file(): + paths.append(f".trellis/spec/{sub.name}/index.md") + continue + for nested in sorted(sub.iterdir()): + if not nested.is_dir(): + continue + nested_index = nested / "index.md" + if nested_index.is_file(): + paths.append(f".trellis/spec/{sub.name}/{nested.name}/index.md") + + return paths + + +def _build_compact_current_state( + trellis_dir: Path, + hook_input: dict, + spec_index_paths: list[str], +) -> str: + repo_root = trellis_dir.parent + lines: list[str] = [] + + try: + from common.paths import get_active_journal_file, get_developer, get_tasks_dir, count_lines # type: ignore[import-not-found] + from common.tasks import iter_active_tasks # type: ignore[import-not-found] + except Exception: + get_active_journal_file = None # type: ignore[assignment] + get_developer = None # type: ignore[assignment] + get_tasks_dir = None # type: ignore[assignment] + count_lines = None # type: ignore[assignment] + iter_active_tasks = None # type: ignore[assignment] + + developer = get_developer(repo_root) if get_developer else None + lines.append(f"Developer: {developer or '(not initialized)'}") + lines.append(_format_git_state(repo_root)) + + active = _resolve_active_task(trellis_dir, hook_input) + if active.task_path: + task_dir = _resolve_task_dir(trellis_dir, active.task_path) + status = "unknown" + task_json = task_dir / "task.json" + if task_json.is_file(): + try: + data = json.loads(task_json.read_text(encoding="utf-8")) + if isinstance(data, dict): + status = str(data.get("status") or "unknown") + except (json.JSONDecodeError, OSError): + pass # Optional task metadata; fall back to generic status. + lines.append(f"Current task: {_repo_relative(repo_root, task_dir)}; status={status}.") + else: + lines.append("Current task: none.") + + if get_tasks_dir and iter_active_tasks: + try: + task_count = sum(1 for _ in iter_active_tasks(get_tasks_dir(repo_root))) + lines.append( + f"Active tasks: {task_count} total. Use `python3 ./.trellis/scripts/task.py list --mine` only if needed." + ) + except Exception: + pass # Optional task summary; keep compact state available. + + if get_active_journal_file and count_lines: + journal = get_active_journal_file(repo_root) + if journal: + lines.append( + f"Journal: {_repo_relative(repo_root, journal)}, {count_lines(journal)} / 2000 lines." + ) + + if spec_index_paths: + lines.append(f"Spec indexes: {len(spec_index_paths)} available.") + + return "\n".join(lines) + + def _extract_range(content: str, start_header: str, end_header: str) -> str: """Extract lines starting at `## start_header` up to (but excluding) `## end_header`.""" lines = content.splitlines() @@ -320,33 +446,25 @@ _BREADCRUMB_TAG_RE = re.compile( def _strip_breadcrumb_tag_blocks(content: str) -> str: - return _BREADCRUMB_TAG_RE.sub("", content) + stripped = _BREADCRUMB_TAG_RE.sub("", content) + stripped = re.sub(r"<!--.*?-->", "", stripped, flags=re.DOTALL) + stripped = re.sub(r"^\[(?!/?workflow-state:)/?[^\]\n]+\]\s*\n?", "", stripped, flags=re.MULTILINE) + return re.sub(r"\n{3,}", "\n\n", stripped).strip() def _build_workflow_toc(workflow_path: Path) -> str: - """Inject workflow guide: TOC + Phase Index + Phase 1/2/3 step details. - - Since v0.5.0-rc.0 the [workflow-state:STATUS] breadcrumb tag blocks - live inside ## Phase Index. They're consumed by inject-workflow-state.py - on each UserPromptSubmit, so strip them from the session-start payload - to avoid duplicating context. - """ + """Inject only the compact Phase Index summary for SessionStart.""" content = read_file(workflow_path) if not content: return "No workflow.md found" out_lines = [ - "# Development Workflow — Section Index", - "Full guide: .trellis/workflow.md (read on demand)", + "# Development Workflow - Session Summary", + "Full guide: .trellis/workflow.md. Step detail: `python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y>`.", "", - "## Table of Contents", ] - for line in content.splitlines(): - if line.startswith("## "): - out_lines.append(line) - out_lines += ["", "---", ""] - phases = _extract_range(content, "Phase Index", "Customizing Trellis (for forks)") + phases = _extract_range(content, "Phase Index", "Phase 1: Plan") if phases: out_lines.append(_strip_breadcrumb_tag_blocks(phases).rstrip()) @@ -370,16 +488,12 @@ def main() -> None: configure_project_encoding(project_dir) trellis_dir = project_dir / ".trellis" - context_key = _resolve_context_key(project_dir, hook_input) + spec_index_paths = _collect_spec_index_paths(trellis_dir) output = StringIO() - output.write(SUB_AGENT_NOTICE) - output.write("\n\n") - output.write("""<session-context> -You are starting a new session in a Trellis-managed project. -Read and follow all instructions below carefully. +Trellis compact SessionStart context. Use it to orient the session; load details on demand. </session-context> """) @@ -387,65 +501,23 @@ Read and follow all instructions below carefully. output.write("\n\n") output.write("<current-state>\n") - context_script = trellis_dir / "scripts" / "get_context.py" - output.write(run_script(context_script, context_key)) + output.write(_build_compact_current_state(trellis_dir, hook_input, spec_index_paths)) output.write("\n</current-state>\n\n") - output.write("<workflow>\n") + output.write("<trellis-workflow>\n") output.write(_build_workflow_toc(trellis_dir / "workflow.md")) - output.write("\n</workflow>\n\n") + output.write("\n</trellis-workflow>\n\n") output.write("<guidelines>\n") output.write( - "Project spec indexes are listed by path below. Each index contains a " - "**Pre-Development Checklist** listing the specific guideline files to " - "read before coding.\n\n" - "- If you're spawning an implement/check sub-agent, context is injected " - "automatically via `{task}/implement.jsonl` / `check.jsonl`. You do NOT " - "need to read these indexes yourself.\n" - "- For agent-capable platforms, the default is to dispatch " - "`trellis-implement` and `trellis-check` (so JSONL context is loaded by " - "the sub-agents) rather than editing code in the main session. " - "Honor a per-turn user override only if the user's current message " - "explicitly opts out (see <task-status> below for override phrases).\n" - "- Sub-agent self-exemption: if you are reading this as a `trellis-implement` " - "or `trellis-check` sub-agent, the \"dispatch trellis-implement / trellis-check\" " - "rule above does NOT apply to you — you are already the dispatched sub-agent. " - "Do NOT spawn another sub-agent of the same kind; implement / check directly.\n\n" + "Task context order for implementation/check: jsonl entries -> `prd.md` -> " + "`design.md if present` -> `implement.md if present`. Missing optional artifacts " + "are skipped for lightweight tasks.\n\n" ) - # guides/ inlined (cross-package thinking, broadly useful) - guides_index = trellis_dir / "spec" / "guides" / "index.md" - if guides_index.is_file(): - output.write("## guides (inlined — cross-package thinking guides)\n") - output.write(read_file(guides_index)) - output.write("\n\n") - - # Other indexes — paths only - paths: list[str] = [] - spec_dir = trellis_dir / "spec" - if spec_dir.is_dir(): - for sub in sorted(spec_dir.iterdir()): - if not sub.is_dir() or sub.name.startswith("."): - continue - if sub.name == "guides": - continue - index_file = sub / "index.md" - if index_file.is_file(): - paths.append(f".trellis/spec/{sub.name}/index.md") - else: - for nested in sorted(sub.iterdir()): - if not nested.is_dir(): - continue - nested_index = nested / "index.md" - if nested_index.is_file(): - paths.append( - f".trellis/spec/{sub.name}/{nested.name}/index.md" - ) - - if paths: - output.write("## Available spec indexes (read on demand)\n") - for p in paths: + if spec_index_paths: + output.write("## Available indexes (read on demand)\n") + for p in spec_index_paths: output.write(f"- {p}\n") output.write("\n") @@ -459,9 +531,7 @@ Read and follow all instructions below carefully. output.write(f"<task-status>\n{task_status}\n</task-status>\n\n") output.write("""<ready> -Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them. -When the user sends the first message, follow <task-status> and the workflow guide. -If a task is READY, execute its Next required action without asking whether to continue. +Context loaded. Follow <task-status>. Load workflow/spec/task details only when needed. </ready>""") context = output.getvalue() diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..bec56db2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# Trellis: append-only developer journals should merge cleanly across +# parallel sessions/worktrees — each session only appends a new block, so +# there is nothing to actually conflict on. +# +# Do NOT add a rule for workspace/*/index.md here — it is fully regenerated +# every session, so a real conflict there is expected and safe to resolve by +# picking either side (task state lives in task.json, not index.md). See +# .trellis/spec/cli/backend/directory-structure.md for details. +.trellis/workspace/*/journal-*.md merge=union diff --git a/.trellis/.template-hashes.json b/.trellis/.template-hashes.json index f58c6e8f..1877fcde 100644 --- a/.trellis/.template-hashes.json +++ b/.trellis/.template-hashes.json @@ -1,77 +1,91 @@ { "__version": 2, "hashes": { - ".agents/skills/trellis-continue/SKILL.md": "aba3e18dc4a4d893ab9b3e3bb830acd111c72aacf319a059955ef9e3097e1117", + ".agents/skills/trellis-continue/SKILL.md": "7723ccf49fbf19d8f086cacc7a080bd8be8db6fc70a32908b80f68efa318d7bf", ".agents/skills/trellis-finish-work/SKILL.md": "161060fbcd44f787440d3a5c297a9f5223ea7774bb3021a50e376875a9ac5b2d", - ".agents/skills/trellis-before-dev/SKILL.md": "208ad3fd5131fa0da603d4bc354a29826967397f5aeef483fa0564113df13e9e", - ".agents/skills/trellis-brainstorm/SKILL.md": "babf6e6e4d0a59d6b7284ff09364d224a3dc299a98911f3a32d0bbcf2c0c21f2", - ".agents/skills/trellis-break-loop/SKILL.md": "35afb53fef42cd494e566f1ef170dbf442ec2be7e19931f28a14079b4dda753f", - ".agents/skills/trellis-check/SKILL.md": "a3f17aef687aa3b475d12ee64c3293e5491bb7474336be2c0f9ec22042f13b6e", + ".agents/skills/trellis-before-dev/SKILL.md": "859894f2e8258cbfa142d710363433761c54984c69f7ed7bd44512fb4eb165cd", + ".agents/skills/trellis-brainstorm/SKILL.md": "a0f226ddcb8a3e846acd2a35d121996e9ca55165ce76202095d0b65e2b48a5e8", + ".agents/skills/trellis-break-loop/SKILL.md": "f5a93699832f29dee443b53c135a7459519b371f689af191d15c29e8ee5c7bde", + ".agents/skills/trellis-check/SKILL.md": "b21ff04b7680ebacb8c5ecbc48a22d627eb13e2b47fceb78c8ced0b43b60b282", ".agents/skills/trellis-update-spec/SKILL.md": "003ce08a3404aeb50998029392c4d4e57b626edf526d3ebd585032bb92dcbb96", ".agents/skills/trellis-meta/references/customize-local/add-project-local-conventions.md": "86009ccb5d0373f399582da0bc570c4e5c6053c3c764857424ff93384f0e04e5", - ".agents/skills/trellis-meta/references/customize-local/change-agents.md": "7f2982162463f107f8b1a4fa1a41fee2bc7dbd0cc8e90c48559aba30c3ea403c", - ".agents/skills/trellis-meta/references/customize-local/change-context-loading.md": "c7be53e038eac99ff4d0abb3fafc67cfdfd90352744ce09ad11e7ef5085cf933", - ".agents/skills/trellis-meta/references/customize-local/change-hooks.md": "91892f2cff53ae003736007e95172945acabf48b2fc889bd627cd2406ce449c4", - ".agents/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "b3009ef20a4f24e5d8b196109dc9bab6bd30fc030dbc4fb796afdd2ca912e1ea", - ".agents/skills/trellis-meta/references/customize-local/change-spec-structure.md": "a3fc9da294448b4a3525ae1ab7c8c9b895ef8f3b53bcf37a13ce6afbdc040fe6", + ".agents/skills/trellis-meta/references/customize-local/change-agents.md": "4216ac3cc570038fd8ce3319a85932bb537fea9e0a7f4d11fa315cfa645c9c85", + ".agents/skills/trellis-meta/references/customize-local/change-context-loading.md": "350d319dc1ab99609ddbf52cf8c06c71bd97ba1a29ce2eac8b97d0bb192938bf", + ".agents/skills/trellis-meta/references/customize-local/change-hooks.md": "4c18b134f05d5ba1609c517ec94c97c9442b5f0670aa2211c487c31ed2a4f358", + ".agents/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "5b942e9f512e049a75e8dd9e8cc4d49786f6da61b22afc82bb447acf808f9fe2", + ".agents/skills/trellis-meta/references/customize-local/change-spec-structure.md": "31eccaad7097d96e66a45c1b4caea1ba4f2e54b7814184c3ebf82c87dafc4841", ".agents/skills/trellis-meta/references/customize-local/change-task-lifecycle.md": "60ff9efb93604b87a461a4af30322d76750402a51e40f31531a7ff88d309996d", - ".agents/skills/trellis-meta/references/customize-local/change-workflow.md": "6f1707a2cc032c50e41e5624cef46071dd53dc9810bc6b3cae66d86508dea1cb", - ".agents/skills/trellis-meta/references/customize-local/overview.md": "1a406c24b4c5737cf517ead5ecb0846c20b0648a117008d3f5a47614fc1793ca", - ".agents/skills/trellis-meta/references/local-architecture/context-injection.md": "31286b9c05e600db7d179100eca533f9b8a4aab3a9c255cb69e8dccacb4e8375", - ".agents/skills/trellis-meta/references/local-architecture/generated-files.md": "4356517517cef0ba7f3ba01965a4ba8953505702e4085f0797d3e36817c9669f", - ".agents/skills/trellis-meta/references/local-architecture/overview.md": "45ffd4ee95020f58201adc885f3dfc89b26483c2b350d96ca7f2f57f94d5ff5f", - ".agents/skills/trellis-meta/references/local-architecture/spec-system.md": "8996504201e2a5460516948fe29c4ce7ed6b960f15da8c514855d2d467046985", - ".agents/skills/trellis-meta/references/local-architecture/task-system.md": "a6f905e53fe5a7fa5be8a4bbe1d102bd90b69c6e63a1a3be51ac3f7f164f1d21", + ".agents/skills/trellis-meta/references/customize-local/change-workflow.md": "43fa780a2ca580de121b10893d49b99f978873deebbf45008c466e5ac6651519", + ".agents/skills/trellis-meta/references/customize-local/overview.md": "ce8f09e9f93ce9a48500763fb3a4db2b3908a5fbf4f985ab71dacebb404cf8f4", + ".agents/skills/trellis-meta/references/local-architecture/context-injection.md": "8497289bf333b3aa456f317039d1239b7ece79254aa0eb62cfc647714c866084", + ".agents/skills/trellis-meta/references/local-architecture/generated-files.md": "7eb2d452eddb4f4226f7578c2ec6d5ee0434ed172ba4c36107cc8bdff7554dc6", + ".agents/skills/trellis-meta/references/local-architecture/overview.md": "50638fd9eaaba2e0edf2f2a84d920578d5b2fb7934031b174e417d3dc510b2a6", + ".agents/skills/trellis-meta/references/local-architecture/spec-system.md": "b8d8a6a0888b44a232c8f50161b9e20e903cf621ad7be4021715ab6fab226f47", + ".agents/skills/trellis-meta/references/local-architecture/task-system.md": "2b561d49c390f7d0db5391912946133be4bf73189231e2b8cc9afa1c5ac6165a", ".agents/skills/trellis-meta/references/local-architecture/workflow.md": "cfcdc6e4468a5d9c816e929fcca01640cd41cfdaaa4824118b40a8e460c927b6", ".agents/skills/trellis-meta/references/local-architecture/workspace-memory.md": "e6427b46aba744563c2444b30df4043cd856561b7709ec2dece26095416421fd", - ".agents/skills/trellis-meta/references/platform-files/agents.md": "ffee78fc3c29114c7409ca4805ebd51d44d8a6acb630e9f73d47013e10db5ac5", - ".agents/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "6e2d6d88719c2779fe34004f63d36cff203d8f64e7fb620f7cb1cde15c37c462", - ".agents/skills/trellis-meta/references/platform-files/overview.md": "6479cd2393166b4b369b511c44b78cbc64975c8b1df96ee1d4d1bd06b75cd48d", - ".agents/skills/trellis-meta/references/platform-files/platform-map.md": "ded6751c06f31d0a701d33c9dd69c482a583539ad3ed464aaad9e705f793b212", - ".agents/skills/trellis-meta/references/platform-files/skills-and-commands.md": "85435eb8bb6921283575bca51268fc534c22fd3ca33782e841ee5c76140ae48f", - ".agents/skills/trellis-meta/SKILL.md": "942e898a6fd769a93a3ca6f43f9fe0412d0adae011654fd384e9cacbd2af4f34", - ".agents/skills/trellis-spec-bootstarp/references/mcp-setup.md": "df542fc8f279edd38046d26a7c8151804b708f57b24d4aa2733cea587a88c65e", - ".agents/skills/trellis-spec-bootstarp/references/repository-analysis.md": "0dae98d774f6e34559b9f3442888ac43e3a8af110c37cbefc49ce256986858b6", - ".agents/skills/trellis-spec-bootstarp/references/spec-task-planning.md": "ef493d028c3b0807a8a534bb71fb92a68129f273db763ad27ceb464a522e799d", - ".agents/skills/trellis-spec-bootstarp/references/spec-writing.md": "e9800fe9ed4a4cd87062ea1829cf2caa8d170ec15e141678a6a30e74c497f47d", - ".agents/skills/trellis-spec-bootstarp/SKILL.md": "81f400092b21392161e7d9dfa9111c9be36c81bc8641d252ed28e08373449ac0", - ".agents/skills/trellis-start/SKILL.md": "a88faaac9fe1b9fa7441473a72ff88ead0022504eeb38492ecef85db75a559e7", - ".codex/agents/trellis-check.toml": "e6781803094ef836869b68fb00b28f0785e9f97091affb5e5bd7b13ab406d6c6", - ".codex/agents/trellis-implement.toml": "b84884c8fe46ecc032ddb287d7aac4ac9553a7c49ab7b6a40ae44a08f6725b58", - ".codex/agents/trellis-research.toml": "5492f7f6ab8bdea975b0e853bf171b050f7ddf6c2079ac770ed912c48d815eae", - ".codex/hooks/session-start.py": "bc3bd73ad89fc872c14efc8a548f4d7ff0b0afd4bb667e9db49fe468dcafd326", - ".codex/hooks/inject-workflow-state.py": "fe4cca4db7ca8c252f614efca16fa588009a4238542a2c9aa85167409f6f9a4c", - ".codex/hooks.json": "522ba3c488c100027783e52ecff84c0bd799852dd77ad3f1936e86db105f01d6", - ".codex/config.toml": "4224eb7df6802a623cb1bee522aed0a23ba6be862b90f1b597a313fc16864b06", + ".agents/skills/trellis-meta/references/platform-files/agents.md": "9f41349b78f7ae64698a38490a317882561f3805b26a79bda587a13d03fda245", + ".agents/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "154af08f7ee8afe7a704968ec0b9fc21e905b7cfb03871c9dbafcdd6cc654319", + ".agents/skills/trellis-meta/references/platform-files/overview.md": "1aec9087ccedd56a213af877b5db474131aa4b4789b6ac0e8da73b058aa43bd4", + ".agents/skills/trellis-meta/references/platform-files/platform-map.md": "9e476e500f10b2a1a05278dd700837deb731a77e3f3993e90f8102528a9bdcca", + ".agents/skills/trellis-meta/references/platform-files/skills-and-commands.md": "e39831d860bd27a04e7757f7bd941ab83b3c5b11ad4460cec03975b655f26cc3", + ".agents/skills/trellis-meta/SKILL.md": "208e92ace3b8d979d8158b3bd0169185f508f2e1424fcd78fcd03cbcc39516a1", + ".agents/skills/trellis-start/SKILL.md": "79a5ba7a2aff3c72e06d7f4cd6942dc4f4f4092dd40f9c8e94f1838024a81e4d", + ".codex/agents/trellis-check.toml": "42d774d175dab732872e1056aa70005b6a1693d93fecfde6b9c86ecc50bd7b9e", + ".codex/agents/trellis-implement.toml": "c21eb1a671ebb8afe476ad54ae9ae1e33e4e97750bfdc4eabecc772a4d4e777b", + ".codex/agents/trellis-research.toml": "200b9fca30a68e027d2e8b2b4c785fafccb9aa75853e70abd92d2d07d7cba5d1", + ".codex/hooks/session-start.py": "14de3be1cf6eb9c9feba348d8998b407f3837d6c0756b74210c9200543440677", + ".codex/hooks/inject-workflow-state.py": "9ce43910ac39cbb0e4d1783fbde931761eb04536c7f82661a96d95ea72c14bde", + ".codex/hooks.json": "85a58ba7cdf1e19e7f75ddcc64e5680180c487ca266a74bd5005f31abeee2e02", + ".codex/config.toml": "9f2d20e28f0bc9c886312eca3ad3bba41533ef4615aaaafe25e98152302267bb", ".trellis/config.yaml": "5c9207418cecc390e9d86d589b4183b831f76697d92fb42fefd5221cd8772e51", ".trellis/scripts/__init__.py": "1242be5b972094c2e141aecbe81a4efd478f6534e3d5e28306374e6a18fcf46c", - ".trellis/scripts/add_session.py": "6e406a0a9f32d4a50b1b5ca8115cbd06c359011f0e166c41dc5fab34698a4006", + ".trellis/scripts/add_session.py": "876dad478edf70db59acccaae9cb4db646a155681f730bd99af48de72ddc9881", ".trellis/scripts/common/__init__.py": "3d5e9347141f0296319a5beb29d69ae714c5a474b9078caeb3edd7c5f6562e22", - ".trellis/scripts/common/active_task.py": "6c88ed40ef7289bca0f6d2ecba0f8b8aef46cd58788080fbeeea88de138a431f", - ".trellis/scripts/common/cli_adapter.py": "cd844d1e84b1a09b373b3a7609e4d5606ee9d4825154c002cc9bb3f54c8e2fb9", - ".trellis/scripts/common/config.py": "25c5a53ad20d6909be5209222e4208a84528805316a4d78350529459a364edb1", + ".trellis/scripts/common/active_task.py": "3fcafde57bdc4442940081d1617295e0f4f8e620d0d4fbb97d33394e8679a39c", + ".trellis/scripts/common/cli_adapter.py": "5d6bd9d6f5c631e7e792db7dd343351317f9643bc87b73a9a98abd51cefb4307", + ".trellis/scripts/common/config.py": "8d2e5f8ccfcd5f622cd2af002aa761f3d3ffcc653182fefb2268afd102e77bca", ".trellis/scripts/common/developer.py": "f5f833123abe68890171b4da825a324216d24913f6b5ad9245afc556424ffd7b", - ".trellis/scripts/common/git.py": "e14817be7de122d3a106f509c2825aeb9669d962ba73ba241642d2931cfdf1d6", + ".trellis/scripts/common/git.py": "ea9ad7686bae598b04e41281ca938f83c06d8021716332947ea51339e7cb1aa9", ".trellis/scripts/common/git_context.py": "fa30ced454f1a91ffc9f8b2abeb32225e3447cbdc90bad783797374eba07265d", - ".trellis/scripts/common/io.py": "6480b181f2bc505323b28ed7a66963d7b7edc96251e83b4c8e7a45907cc721c8", + ".trellis/scripts/common/io.py": "75648caae03d5b1107d7aeccaa785d133b25762266e54a520d90ca8c76b43bdb", ".trellis/scripts/common/log.py": "471df6895cfac80f995edebbf9974f6b7440634b7a688f28b8331c868bc0f3cf", ".trellis/scripts/common/packages_context.py": "efe158d7c99c2268851d0216fbb08de22836e418a8dbeb73575b8cc249eed7b7", ".trellis/scripts/common/paths.py": "05898ef136cc7c4d861b05fbf2b16d53ddd3e6f311a231d4fcfcb81bde7c45ee", - ".trellis/scripts/common/safe_commit.py": "8789bff4b30a9065469210f2efab3f59f03dddd77bef4e4b6a5bb641f93539f4", - ".trellis/scripts/common/session_context.py": "c91cf213025e5e4ad2f56bb6f570ef43d35430fc8a863be311d3e5f8f32ef652", - ".trellis/scripts/common/task_context.py": "1c16a7fa82d363010d0d0ebdc038296ae1552bf6e90214787d707f49567bc159", + ".trellis/scripts/common/safe_commit.py": "baa5c82324eb62154374ec63394ecdc8609bb37d93892e3bcb88f452bb7d6446", + ".trellis/scripts/common/session_context.py": "19927c62d3fd70fecf70da0c5b25c6c81e63683f8a594a1ae3b5d6c115070368", + ".trellis/scripts/common/task_context.py": "51301e180ce702c6a5b1a2dd1b155a6eed1bf2b05cb2ac1e51081212628b11d4", ".trellis/scripts/common/task_queue.py": "0be61f713462b1fe4574927c82fc4704e678afe72dcb9813543aedf2f9e9e0c5", - ".trellis/scripts/common/task_store.py": "cca4851c948df4a7d8dac38c465dce0d6b15a171001adab8dfef011ad66e02e5", - ".trellis/scripts/common/task_utils.py": "f5ef4af87ba3e11d8b19630c0c96d009de1811fc9be56c2027a9c96e21ed103e", + ".trellis/scripts/common/task_store.py": "e3c2fbf8b79b591e39fc3c9f4e2f3ee0c840c8201c94a16709ec743fa45037f6", + ".trellis/scripts/common/task_utils.py": "90c0a6d50bad502c3f01cb24c1ccfeb0eece5e2c69efaff8d65eb827fba43871", ".trellis/scripts/common/tasks.py": "4436a8b0b53c270a35989e26d9dbd92669408c6562d88c02083a404562da85fe", - ".trellis/scripts/common/trellis_config.py": "0839dcf90ebbd77712c276930a89335b3313927051650c91d220fb51ca2a6a3c", + ".trellis/scripts/common/trellis_config.py": "e282e897183e3ec2f4e6e56349431946e5f98c1c31d3eca4de7fc44e1383a7bf", ".trellis/scripts/common/types.py": "9962081cc2608fb9d1deb32c6880e336f62cdca6b338e7ae813304701e155ee9", - ".trellis/scripts/common/workflow_phase.py": "2b260f4a7770e9c3223129836716bd8e2c0f0568acd682224a57415bc1dc726b", + ".trellis/scripts/common/workflow_phase.py": "79ee522de20246acf1e2c222e8ad180ad25aaec7fec98214a93d9e81b350d9a8", ".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/task.py": "40abdd46f5c2b6837610429a38eef50f1fc783fb1852dc4f52a891205e42ab04", - ".trellis/workflow.md": "f4b8a6f89017f62071986d6d36cc32c4c7f01fe6f023f0fd9311eddc57d8c94f" + ".trellis/scripts/task.py": "e0ffed9f14994069f0c992141e3ec168524be5af32e3681e6ea30ba0a5da4bc4", + ".trellis/workflow.md": "f4b8a6f89017f62071986d6d36cc32c4c7f01fe6f023f0fd9311eddc57d8c94f", + ".trellis/agents/implement.md": "66e25ad046c94869442834bc3cdfbd5a9a7412d3ff54561d64d2886552c27e87", + ".trellis/agents/check.md": "edb4f57361407249a53bf5998ebf91c40d2b969e826a2c5e1b4e813a08bcb175", + ".agents/skills/trellis-channel/references/command-reference.md": "c4df3d940d89814310bfdaded20e84dd1d6b96786f6772688de7a123338d3c8e", + ".agents/skills/trellis-channel/references/forum.md": "407db39e8ebde47cea8637e3069bdb3d76ba42b43ed1d08ba97a56f26dbcfcb4", + ".agents/skills/trellis-channel/references/progress-debugging.md": "93b021fd8d5d3d4ee54952c40424ab91ca57c2ab8032bfbfc3b5c5e22f1464cd", + ".agents/skills/trellis-channel/references/workers.md": "d4481b2858bca82050cb8d131916da8a2ccc3857f199cdb9ced8bb41d592e2a7", + ".agents/skills/trellis-channel/references/workflows.md": "0782d602efde6e94a7d7d7950fd605eedc9a1f5a523908284e76b628fd618fd4", + ".agents/skills/trellis-channel/SKILL.md": "ff84801c511a736da018b4b1149348ebc9a16b7fc18d905ec10cfa7acbf269a3", + ".agents/skills/trellis-meta/references/local-architecture/bundled-skills.md": "7a8d1a5dcc8d1140c4c6bd19d02949364cdd01801bd0825a975a308cf85b8f37", + ".agents/skills/trellis-meta/references/local-architecture/multi-agent-channel.md": "56e5070474aeca872e2d70c46feea5aaafecd3d3ec052c3f7b1877358dca62e9", + ".agents/skills/trellis-session-insight/references/cli-quick-reference.md": "c520353fe3fc00b9702ed4f780647c8fbd6d342e66527a03647f533a9fe09779", + ".agents/skills/trellis-session-insight/references/triggering-patterns.md": "121ecd23be83d1567e8ce15c366a81073d7a2b1d3ad616fce235c07ca1f1cc20", + ".agents/skills/trellis-session-insight/SKILL.md": "d20f1d20c6946e26ba6f848b9a016d9a16c803d87501462e5b11a2ced6f56643", + ".agents/skills/trellis-spec-bootstrap/references/mcp-setup.md": "df542fc8f279edd38046d26a7c8151804b708f57b24d4aa2733cea587a88c65e", + ".agents/skills/trellis-spec-bootstrap/references/repository-analysis.md": "0dae98d774f6e34559b9f3442888ac43e3a8af110c37cbefc49ce256986858b6", + ".agents/skills/trellis-spec-bootstrap/references/spec-task-planning.md": "ef493d028c3b0807a8a534bb71fb92a68129f273db763ad27ceb464a522e799d", + ".agents/skills/trellis-spec-bootstrap/references/spec-writing.md": "e9800fe9ed4a4cd87062ea1829cf2caa8d170ec15e141678a6a30e74c497f47d", + ".agents/skills/trellis-spec-bootstrap/SKILL.md": "97bfa68c06cebb558eb4464bc1b81f7d2d56040d75baa8de1ee5ad90cca0196a", + ".codex/hooks/inject-subagent-context.py": "fe7e315b6ef5ca28dafa9a92072e4a07278492ee9cab74d42f0c0b6addf51662" } } \ No newline at end of file diff --git a/.trellis/.version b/.trellis/.version index aa49d36a..18640022 100644 --- a/.trellis/.version +++ b/.trellis/.version @@ -1 +1 @@ -0.5.19 \ No newline at end of file +0.6.9 \ No newline at end of file diff --git a/.trellis/agents/check.md b/.trellis/agents/check.md new file mode 100644 index 00000000..6c1bf137 --- /dev/null +++ b/.trellis/agents/check.md @@ -0,0 +1,70 @@ +--- +name: check +description: | + Code quality auditor for the Trellis channel runtime. Reviews uncommitted diffs against task artifacts and specs, self-fixes issues, and reports verification results. +provider: claude +labels: [trellis, check] +--- + +# Check Agent (channel runtime) + +You are the Check Agent spawned by `trellis channel spawn --agent check` inside the Trellis channel runtime. You receive an `Active task: <path>` line in your inbox; use it to locate task artifacts on disk. + +## Context + +Before reviewing, read in this order: + +1. `<task-path>/check.jsonl` if present — spec manifest curated for this turn; read every listed file +2. `<task-path>/prd.md` — requirements +3. `<task-path>/design.md` if present — technical design +4. `<task-path>/implement.md` if present — execution plan +5. `.trellis/spec/` — project-wide guidelines (load only what is relevant to the diff under review) + +## Core Responsibilities + +1. **Get the diff** — `git diff` / `git diff --staged` for uncommitted changes +2. **Review against task artifacts** — does the diff satisfy `prd.md` (and `design.md` / `implement.md` if present)? +3. **Review against specs** — naming, structure, type safety, error handling, conventions in `.trellis/spec/` +4. **Self-fix** — when an issue is mechanical and small, fix it directly with the editing tools you have +5. **Run verification** — project lint and typecheck on the changed scope +6. **Report** — concrete findings with `file:line` citations and what was fixed vs. what is open + +## Forbidden Operations + +- `git commit` +- `git push` +- `git merge` + +The supervising main session owns commits. Report the post-fix state; do not commit on its behalf. + +## Workflow + +1. Run `git diff --name-only` and `git diff` to scope the changes +2. Read the task artifacts and relevant spec files +3. For each issue: + - If mechanical (lint nit, missing type, wrong import, dead branch) → fix in-place + - If a design/judgment issue → record and report, do not silently rewrite +4. Run the project's lint and typecheck on the changed scope after self-fixes +5. Report + +## Report Format + +``` +## Self-Check Complete + +### Files Checked +- <path> + +### Issues Found and Fixed +1. `<file>:<line>` — <what was wrong> → <what you changed> + +### Issues Not Fixed +- `<file>:<line>` — <issue> — <why deferred to the main session> + +### Verification Results +- TypeCheck: <pass|fail|skipped + reason> +- Lint: <pass|fail|skipped + reason> + +### Summary +Checked <N> files, found <X> issues, fixed <Y>, <X-Y> open. +``` diff --git a/.trellis/agents/implement.md b/.trellis/agents/implement.md new file mode 100644 index 00000000..3262f790 --- /dev/null +++ b/.trellis/agents/implement.md @@ -0,0 +1,71 @@ +--- +name: implement +description: | + Code implementation expert for the Trellis channel runtime. Understands specs and task artifacts, then implements features. No git commit allowed. +provider: claude +labels: [trellis, implement] +--- + +# Implement Agent (channel runtime) + +You are the Implement Agent spawned by `trellis channel spawn --agent implement` inside the Trellis channel runtime. You receive an `Active task: <path>` line in your inbox; use it to locate task artifacts on disk. + +## Context + +Before implementing, read in this order: + +1. `<task-path>/implement.jsonl` if present — spec manifest curated for this turn; read every listed file +2. `<task-path>/prd.md` — requirements +3. `<task-path>/design.md` if present — technical design +4. `<task-path>/implement.md` if present — execution plan +5. `.trellis/spec/` — project-wide guidelines (load only what is relevant to the diff you are about to write) + +## Core Responsibilities + +1. **Understand specs** — read relevant spec files in `.trellis/spec/` +2. **Understand task artifacts** — read the artifacts listed above +3. **Implement features** — write code that follows specs and existing patterns +4. **Self-check** — run lint and typecheck on the changed scope before reporting + +## Forbidden Operations + +- `git commit` +- `git push` +- `git merge` + +The supervising main session owns commits. Report what changed; do not commit on its behalf. + +## Workflow + +1. Read relevant specs based on task type and the files in `implement.jsonl` if present +2. Read the task's `prd.md`, `design.md` if present, and `implement.md` if present +3. Implement features following specs and existing patterns +4. Run the project's lint and typecheck commands on the changed scope +5. Report files touched, key decisions, and verification results back to the channel + +## Code Standards + +- Follow existing code patterns +- Don't add unnecessary abstractions +- Only do what the PRD asks for; no speculative scope expansion +- Surface uncertainty back to the channel rather than guessing + +## Report Format + +``` +## Implementation Complete + +### Files Modified +- <path> — <one-line description> + +### Implementation Summary +1. <step> +2. <step> + +### Verification Results +- Lint: <pass|fail|skipped + reason> +- TypeCheck: <pass|fail|skipped + reason> + +### Open Questions +- <if any, otherwise omit> +``` diff --git a/.trellis/config.yaml b/.trellis/config.yaml index 49f1a7e9..c9bc8b84 100644 --- a/.trellis/config.yaml +++ b/.trellis/config.yaml @@ -74,12 +74,62 @@ session_auto_commit: false #------------------------------------------------------------------------------- # Codex (dispatch behavior) #------------------------------------------------------------------------------- -# Codex-only knob; other platforms ignore it. Default ("inline") makes the -# main Codex agent edit code directly because Codex sub-agents run with -# `fork_turns="none"` isolation and can't inherit the parent session's -# task context. Set to "sub-agent" to opt into the legacy dispatch model -# (main agent spawns trellis-implement / trellis-check / trellis-research -# sub-agents). +# Codex-only knob; other platforms ignore it. Trellis 0.6.9 supports native +# sub-agent context injection, but hyb-oa deliberately keeps the main session +# in charge unless a task explicitly approves sub-agent work. "sub-agent" is +# retained upstream as a backwards-compatible alias for "auto". # codex: dispatch_mode: inline # keep Codex main-session edits until a task explicitly asks for sub-agents + +#------------------------------------------------------------------------------- +# Channel worker OOM guard +#------------------------------------------------------------------------------- +# Default safeguards for `trellis channel spawn` workers. The guard runs +# at spawn time (cleans expired idle workers, then enforces the live-worker +# budget) and inside each supervisor (self-terminates a worker that stays +# continuously idle past `idle_timeout`). +# +# Precedence: CLI flag > env var (TRELLIS_CHANNEL_WORKER_IDLE_TIMEOUT / +# TRELLIS_CHANNEL_MAX_LIVE_WORKERS) > this config > built-in default. +# +# `idle_timeout: 0` disables idle cleanup (workers can sit idle forever +# unless explicitly killed or given `--timeout`). +# `max_live_workers: 0` disables the spawn-time budget check. +# +# `trusted_context_dirs` extends the `--file`/`--jsonl`/`--agent` containment +# check beyond the worker cwd — useful when `.trellis/tasks` or +# `.trellis/workspace` is a symlink to an external directory. Realpaths under +# any listed dir are accepted in addition to cwd. +# `auto_trust_trellis_symlinks: false` disables the narrow auto-trust of +# `.trellis/tasks` / `.trellis/workspace` when either is itself a top-level +# symlink (auto-trust is on by default). +# +channel: + worker_guard: + idle_timeout: 5m + max_live_workers: 6 + # trusted_context_dirs: + # - /work/user/trellis_workspace + # auto_trust_trellis_symlinks: false + +#------------------------------------------------------------------------------- +# Sub-agent context injection limits +#------------------------------------------------------------------------------- +# Trellis 0.6.9 caps injected task artifacts and referenced context. Defaults +# are sufficient for hyb-oa; keep them documented but implicit until a real +# task proves they need tuning. +# +# context_injection: +# max_file_bytes: 32768 +# max_artifact_bytes: 65536 +# max_total_bytes: 131072 + +#------------------------------------------------------------------------------- +# Per-turn prompt injection +#------------------------------------------------------------------------------- +# Optional one-turn escape hatch. The project does not enable it by default so +# Trellis task state remains deterministic. +# +# prompt_injection: +# skip_keyword: "no-trellis" diff --git a/.trellis/scripts/add_session.py b/.trellis/scripts/add_session.py index 60c653ed..6cd66362 100755 --- a/.trellis/scripts/add_session.py +++ b/.trellis/scripts/add_session.py @@ -12,9 +12,12 @@ Usage: <session content here> EOF + # Structured content (repeatable; a section with no bullets is omitted): + python3 add_session.py --title "Title" --change "Did X" --test "Ran Y" --next-step "Do Z" + Branch resolution order: 1. --branch CLI arg (explicit) - 2. task.json branch field (from active task) + 2. task.json branch field (from active task, if still exists) 3. git branch --show-current (auto-detect) 4. None (omitted gracefully) """ @@ -28,6 +31,8 @@ from datetime import datetime from pathlib import Path from common.paths import ( + DIR_TASKS, + DIR_WORKFLOW, FILE_JOURNAL_PREFIX, get_repo_root, get_current_task, @@ -36,12 +41,14 @@ from common.paths import ( ) from common.developer import ensure_developer from common.git import run_git +from common.log import Colors, colored from common.safe_commit import ( print_gitignore_warning, safe_git_add, safe_trellis_paths_to_add, ) from common.tasks import load_task +from common.types import TaskInfo from common.config import ( get_packages, get_session_auto_commit, @@ -124,6 +131,99 @@ def count_journal_files(dev_dir: Path, active_num: int) -> str: return "\n".join(result_lines) +def get_current_git_branch(repo_root: Path) -> str | None: + """Return the current checkout branch, or None for detached/non-git states.""" + rc, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root) + if rc != 0: + return None + detected = branch_out.strip() + return detected or None + + +def branch_ref_exists(repo_root: Path, branch: str) -> bool: + """Return True when branch exists locally or as the local origin ref.""" + for ref in (f"refs/heads/{branch}", f"refs/remotes/origin/{branch}"): + rc, _, _ = run_git(["show-ref", "--verify", "--quiet", ref], cwd=repo_root) + if rc == 0: + return True + return False + + +def resolve_session_branch( + repo_root: Path, + cli_branch: str | None, + task_data: TaskInfo | None, +) -> str | None: + """Resolve journal branch without trusting stale task.json branch fields.""" + if cli_branch: + return cli_branch + + current_branch = get_current_git_branch(repo_root) + raw_task_branch = task_data.raw.get("branch") if task_data else None + task_branch = raw_task_branch.strip() if isinstance(raw_task_branch, str) else "" + if not task_branch: + return current_branch + + if branch_ref_exists(repo_root, task_branch): + return task_branch + + if current_branch: + print( + f"Warning: task.json branch '{task_branch}' no longer exists locally or as origin/{task_branch}; using current branch '{current_branch}'.", + file=sys.stderr, + ) + return current_branch + + print( + f"Warning: task.json branch '{task_branch}' no longer exists locally or as origin/{task_branch}; omitting branch.", + file=sys.stderr, + ) + return None + + +def is_git_worktree(repo_root: Path) -> bool: + """Return True when repo_root is a linked worktree (not the main working tree). + + Standard test: `git rev-parse --git-dir` (per-worktree) differs from + `git rev-parse --git-common-dir` (shared across all worktrees) once both + are resolved to absolute paths. In the main working tree these are the + same directory. + """ + rc_dir, git_dir, _ = run_git(["rev-parse", "--git-dir"], cwd=repo_root) + rc_common, git_common_dir, _ = run_git( + ["rev-parse", "--git-common-dir"], cwd=repo_root + ) + if rc_dir != 0 or rc_common != 0: + return False + + git_dir_path = (repo_root / git_dir.strip()).resolve() + git_common_dir_path = (repo_root / git_common_dir.strip()).resolve() + return git_dir_path != git_common_dir_path + + +def warn_if_parallel_worktree(repo_root: Path) -> None: + """Non-blocking note: index.md conflicts across parallel worktrees/branches + are expected and safe. Only fires when running in a linked git worktree + (not the main tree) with `session_auto_commit` enabled (#415 quick-fix tier). + """ + if not get_session_auto_commit(repo_root): + return + if not is_git_worktree(repo_root): + return + print( + colored( + "[NOTE] Running in a git worktree with session_auto_commit enabled: " + "journal-*.md files auto-merge via .gitattributes, but index.md " + "conflicts across parallel worktrees/branches are expected and safe " + "to resolve by picking either side (task state lives in task.json, " + "not index.md). See .trellis/spec/cli/backend/directory-structure.md " + '("Workspace Journal Merge Behavior").', + Colors.YELLOW, + ), + file=sys.stderr, + ) + + def create_new_journal_file( dev_dir: Path, num: int, developer: str, today: str, max_lines: int = 2000, ) -> Path: @@ -143,15 +243,39 @@ def create_new_journal_file( return new_file +def _render_bullet_section(header: str, items: list[str], bullet_prefix: str = "- ") -> str: + """Render a Markdown section as bullets, or "" when there is no content. + + A section with zero provided values is omitted entirely from the + rendered entry rather than falling back to a placeholder string. + """ + if not items: + return "" + bullets = "\n".join(f"{bullet_prefix}{item}" for item in items) + return f"\n\n### {header}\n\n{bullets}" + + +def _render_main_changes(changes: list[str], extra_content: str | None) -> str: + """Render the Main Changes section from --change bullets or freeform content.""" + if changes: + return _render_bullet_section("Main Changes", changes) + if extra_content: + return f"\n\n### Main Changes\n\n{extra_content}" + return "" + + 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, + changes: list[str] | None = None, + extra_content: str | None = None, + tests: list[str] | None = None, + next_steps: list[str] | None = None, ) -> str: """Generate session content.""" if commit and commit != "-": @@ -166,6 +290,10 @@ def generate_session_content( package_line = f"\n**Package**: {package}" if package else "" branch_line = f"\n**Branch**: `{branch}`" if branch else "" + main_changes_section = _render_main_changes(changes or [], extra_content) + testing_section = _render_bullet_section("Testing", tests or [], bullet_prefix="- [OK] ") + next_steps_section = _render_bullet_section("Next Steps", next_steps or []) + return f""" ## Session {session_num}: {title} @@ -175,27 +303,15 @@ def generate_session_content( ### Summary -{summary} - -### Main Changes - -{extra_content} +{summary}{main_changes_section} ### Git Commits -{commit_table} - -### Testing - -- [OK] (Add test results) +{commit_table}{testing_section} ### Status -[OK] **Completed** - -### Next Steps - -- None - task complete +[OK] **Completed**{next_steps_section} """ @@ -319,12 +435,14 @@ def update_index( # ============================================================================= def _auto_commit_workspace(repo_root: Path) -> None: - """Stage Trellis-owned workspace + task paths and commit. + """Stage Trellis-owned workspace + current-task paths and commit. - Path scope is restricted to specific products (journal files, index.md, - active task dirs, the archive subtree). We never `git add` the whole - `.trellis/` tree, and if `.gitignore` blocks the specific paths we - warn + skip — never retry with ``-f``. + Path scope is restricted to specific products: the current developer's + journal files + index.md, and ONLY the current task directory (resolved + via ``get_current_task``). We never `git add` the whole `.trellis/` tree + or iterate over all active task dirs (#303: parallel-window dirty task + dirs must not be bundled into the session auto-commit). If `.gitignore` + blocks the specific paths we warn + skip — never retry with ``-f``. Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when set to ``false``, this function returns immediately without touching git @@ -338,7 +456,23 @@ def _auto_commit_workspace(repo_root: Path) -> None: return commit_msg = get_session_commit_message(repo_root) - paths = safe_trellis_paths_to_add(repo_root) + # Resolve the current task so staging is scoped to its dir only. The ref + # is ``.trellis/tasks/<name>`` (or under archive/) — pass the bare name. + current = get_current_task(repo_root) + if current: + task_name = Path(current).name + paths = safe_trellis_paths_to_add(repo_root, task_name=task_name) + else: + # Current task unknown (0 or >=2 parallel sessions — exactly the + # parallel-window case #303 is about). Do NOT fall back to the wide + # `tasks_dir.iterdir()` scan; that would re-leak other tasks' dirty + # dirs into the session commit. Stage only the developer's journal/ + # index and skip every task dir. + paths = [ + p + for p in safe_trellis_paths_to_add(repo_root, task_name=None) + if not p.startswith(f"{DIR_WORKFLOW}/{DIR_TASKS}/") + ] if not paths: print("[OK] No workspace changes to commit.", file=sys.stderr) return @@ -375,14 +509,18 @@ def _auto_commit_workspace(repo_root: Path) -> None: def add_session( title: str, commit: str = "-", - summary: str = "(Add summary)", - extra_content: str = "(Add details)", + summary: str = "Session summary was not supplied.", + changes: list[str] | None = None, + extra_content: str | None = None, + tests: list[str] | None = None, + next_steps: list[str] | None = None, auto_commit: bool = True, package: str | None = None, branch: str | None = None, ) -> int: """Add a new session.""" repo_root = get_repo_root() + warn_if_parallel_worktree(repo_root) ensure_developer(repo_root) developer = get_developer(repo_root) @@ -405,8 +543,9 @@ def add_session( new_session = current_session + 1 session_content = generate_session_content( - new_session, title, commit, summary, extra_content, today, package, - branch, + new_session, title, commit, summary, today, package, branch, + changes=changes, extra_content=extra_content, tests=tests, + next_steps=next_steps, ) content_lines = len(session_content.splitlines()) @@ -483,10 +622,13 @@ def main() -> int: ) 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("--summary", default="Session summary was not supplied.", 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("--change", action="append", help="Main Changes bullet (repeatable)") + parser.add_argument("--test", action="append", help="Testing bullet (repeatable)") + parser.add_argument("--next-step", action="append", help="Next Steps bullet (repeatable)") parser.add_argument("--no-commit", action="store_true", help="Skip auto-commit of workspace changes") parser.add_argument("--stdin", action="store_true", @@ -494,7 +636,7 @@ def main() -> int: args = parser.parse_args() - extra_content = "(Add details)" + extra_content: str | None = None if args.content_file: content_path = Path(args.content_file) if content_path.is_file(): @@ -523,20 +665,12 @@ def main() -> int: 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 + branch = resolve_session_branch(repo_root, args.branch, task_data) return add_session( - args.title, args.commit, args.summary, extra_content, + args.title, args.commit, args.summary, + changes=args.change, extra_content=extra_content, tests=args.test, + next_steps=args.next_step, auto_commit=not args.no_commit, package=package, branch=branch, diff --git a/.trellis/scripts/common/active_task.py b/.trellis/scripts/common/active_task.py index e6597e85..c439fc7c 100755 --- a/.trellis/scripts/common/active_task.py +++ b/.trellis/scripts/common/active_task.py @@ -43,6 +43,11 @@ _KNOWN_PLATFORMS = { "kiro", "copilot", "pi", + "trae", + "grok", + "kimi", + "zcode", + "snow", } _ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = ( @@ -57,6 +62,15 @@ _ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = ( ("kiro", ("KIRO_SESSION_ID",)), ("copilot", ("COPILOT_SESSION_ID", "COPILOT_SESSIONID")), ("pi", ("PI_SESSION_ID", "PI_SESSIONID")), + ("trae", ("TRAE_SESSION_ID",)), + # ZCode reuses CLAUDE_SESSION_ID (it does not document a ZCODE_SESSION_ID). + # Platform-scoped lookup (_iter_env_keys filters by platform name), so this + # only fires when the resolver already detected "zcode" — no collision with + # the claude entry above. + ("zcode", ("CLAUDE_SESSION_ID",)), + # Snow CLI exports SNOW_SESSION_ID into hook/terminal/sub-agent children. + # TRELLIS_CONTEXT_ID remains the preferred override when present. + ("snow", ("SNOW_SESSION_ID",)), ) _ENV_CONVERSATION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = ( ("cursor", ("CURSOR_CONVERSATION_ID", "CURSOR_CONVERSATIONID")), @@ -76,6 +90,12 @@ _ENV_PLATFORM_ALIASES = { "factory-ai": "droid", "github-copilot": "copilot", } +# ZCode intentionally reuses CLAUDE_SESSION_ID. Hooks know the host is ZCode, +# while later shell commands see only the shared env name and resolve it through +# the Claude entry. Canonicalize both paths to one runtime filename. +_CONTEXT_KEY_PLATFORM_ALIASES = { + "zcode": "claude", +} @dataclass(frozen=True) @@ -189,6 +209,7 @@ def _detect_platform(platform_input: dict[str, Any] | None, platform: str | None def _context_key(platform_name: str, kind: str, value: str) -> str: + platform_name = _CONTEXT_KEY_PLATFORM_ALIASES.get(platform_name, platform_name) if kind == "transcript": return f"{platform_name}_transcript_{_hash_value(value)}" safe_value = _sanitize_key(value) @@ -380,15 +401,18 @@ def _lookup_cursor_shell_ticket_context_key() -> str | None: def resolve_context_key( platform_input: dict[str, Any] | None = None, platform: str | None = None, + *, + allow_environment_context: bool = True, ) -> str | None: """Resolve a stable session/window context key, if one is available. `TRELLIS_CONTEXT_ID` is an explicit context-key override used by CLI scripts and subprocesses. It does not store the task itself. """ - override = _string_value(os.environ.get("TRELLIS_CONTEXT_ID")) - if override: - return _sanitize_key(override) or _hash_value(override) + if allow_environment_context: + override = _string_value(os.environ.get("TRELLIS_CONTEXT_ID")) + if override: + return _sanitize_key(override) or _hash_value(override) data = _as_dict(platform_input) platform_name = _detect_platform(data, platform) if data or platform else None @@ -406,11 +430,12 @@ def resolve_context_key( if transcript_path: return _context_key(platform_name or "session", "transcript", transcript_path) - env_context_key = _lookup_env_context_key(platform_name) - if env_context_key: - return env_context_key + if allow_environment_context: + env_context_key = _lookup_env_context_key(platform_name) + if env_context_key: + return env_context_key - if platform_name in (None, "session", "cursor"): + if allow_environment_context and platform_name in (None, "session", "cursor"): return _lookup_cursor_shell_ticket_context_key() return None @@ -469,17 +494,24 @@ def resolve_active_task( repo_root: Path, platform_input: dict[str, Any] | None = None, platform: str | None = None, + *, + allow_single_session_fallback: bool = True, + allow_environment_context: bool = True, ) -> ActiveTask: """Resolve the active task from session runtime state only. A stale session task is returned as stale. Missing context identity or a missing/empty session context falls back to single-session inference: if exactly one session file exists in the runtime, return its task with - source_type="session-fallback" — covers class-2 platform sub-agents (codex, - copilot, gemini, qoder) that don't inherit the parent's session id. ≥2 + source_type="session-fallback" — covers pull-based platform sub-agents + (copilot, gemini, qoder) that don't inherit the parent's session id. ≥2 files or 0 files yield ActiveTask(None) — refuses to guess across windows. """ - context_key = resolve_context_key(platform_input, platform) + context_key = resolve_context_key( + platform_input, + platform, + allow_environment_context=allow_environment_context, + ) if context_key: context = _read_json(_context_path(repo_root, context_key)) or {} task_ref = _string_value(context.get("current_task")) @@ -487,9 +519,10 @@ def resolve_active_task( if active: return active - fallback = _resolve_single_session_fallback(repo_root) - if fallback is not None: - return fallback + if allow_single_session_fallback: + fallback = _resolve_single_session_fallback(repo_root) + if fallback is not None: + return fallback return ActiveTask(None, "none", context_key) diff --git a/.trellis/scripts/common/cli_adapter.py b/.trellis/scripts/common/cli_adapter.py index b65f61a2..85e1c3a8 100755 --- a/.trellis/scripts/common/cli_adapter.py +++ b/.trellis/scripts/common/cli_adapter.py @@ -1,7 +1,7 @@ """ CLI Adapter for Multi-Platform Support. -Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Windsurf, Qoder, CodeBuddy, GitHub Copilot, Factory Droid, and Pi Agent interfaces. +Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Devin, Qoder, CodeBuddy, GitHub Copilot, Factory Droid, and Pi Agent interfaces. Supported platforms: - claude: Claude Code (default) @@ -13,12 +13,16 @@ Supported platforms: - kiro: Kiro Code (skills-based) - gemini: Gemini CLI - antigravity: Antigravity (workflow-based) -- windsurf: Windsurf (workflow-based) +- devin: Devin (formerly Windsurf; workflow-based) - qoder: Qoder - codebuddy: CodeBuddy - copilot: GitHub Copilot (VS Code) - droid: Factory Droid (commands-based) - pi: Pi Agent (extension-backed) +- trae: Trae IDE (IDE-only, hooks-based) +- omp: Oh My Pi +- grok: Grok Build (pull-based skills/agents; no hook context injection) +- kimi: Kimi Code (pull-based skills; commands delivered as skills; no hook context injection) Usage: from common.cli_adapter import CLIAdapter @@ -47,12 +51,16 @@ Platform = Literal[ "kiro", "gemini", "antigravity", - "windsurf", + "devin", "qoder", "codebuddy", "copilot", "droid", "pi", + "trae", + "omp", + "grok", + "kimi", ] @@ -97,7 +105,7 @@ class CLIAdapter: """Get platform-specific config directory name. Returns: - Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.windsurf', '.qoder', '.codebuddy', '.github/copilot', '.factory', or '.pi') + Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.devin', '.qoder', '.codebuddy', '.github/copilot', '.factory', '.pi', or '.trae') """ if self.platform == "opencode": return ".opencode" @@ -115,8 +123,8 @@ class CLIAdapter: return ".gemini" elif self.platform == "antigravity": return ".agent" - elif self.platform == "windsurf": - return ".windsurf" + elif self.platform == "devin": + return ".devin" elif self.platform == "qoder": return ".qoder" elif self.platform == "codebuddy": @@ -127,6 +135,14 @@ class CLIAdapter: return ".factory" elif self.platform == "pi": return ".pi" + elif self.platform == "trae": + return ".trae" + elif self.platform == "omp": + return ".omp" + elif self.platform == "grok": + return ".grok" + elif self.platform == "kimi": + return ".kimi-code" else: return ".claude" @@ -137,7 +153,7 @@ class CLIAdapter: project_root: Project root directory Returns: - Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .windsurf, .qoder, .codebuddy, .github/copilot, .factory, or .pi) + Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .devin, .qoder, .codebuddy, .github/copilot, .factory, .pi, or .trae) """ return project_root / self.config_dir_name @@ -169,7 +185,7 @@ class CLIAdapter: 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 + Devin uses workflow directory: .devin/workflows/trellis-<name>.md Copilot uses prompt files: .github/prompts/<name>.prompt.md Pi uses prompt templates: .pi/prompts/trellis-<name>.md Claude/OpenCode use subdirectory: .claude/commands/trellis/<name>.md @@ -184,8 +200,31 @@ class CLIAdapter: filename = filename[:-3] return prompts_dir / f"trellis-{filename}.md" return prompts_dir / Path(*parts) + # OMP and Grok: flat slash commands under .{platform}/commands/trellis-<name>.md + if self.platform in ("omp", "grok"): + commands_dir = self.get_config_dir(project_root) / "commands" + if not parts: + return commands_dir + if len(parts) >= 2 and parts[0] == "trellis": + filename = parts[-1] + if filename.endswith(".md"): + filename = filename[:-3] + return commands_dir / f"trellis-{filename}.md" + return commands_dir / Path(*parts) - if self.platform == "windsurf": + # Kimi: commands are skills under .kimi-code/skills/trellis-<name>/SKILL.md + if self.platform == "kimi": + skills_dir = self.get_config_dir(project_root) / "skills" + if not parts: + return skills_dir + if len(parts) >= 2 and parts[0] == "trellis": + filename = parts[-1] + if filename.endswith(".md"): + filename = filename[:-3] + return skills_dir / f"trellis-{filename}" / "SKILL.md" + return skills_dir / Path(*parts) + + if self.platform == "devin": workflow_dir = self.get_config_dir(project_root) / "workflows" if not parts: return workflow_dir @@ -242,7 +281,7 @@ class CLIAdapter: Kiro: .kiro/skills/trellis-<name>/SKILL.md Gemini: .gemini/commands/trellis/<name>.toml Antigravity: .agent/workflows/<name>.md - Windsurf: .windsurf/workflows/trellis-<name>.md + Devin: .devin/workflows/trellis-<name>.md Pi: .pi/prompts/trellis-<name>.md Others: .{platform}/commands/trellis/<name>.md """ @@ -258,8 +297,8 @@ class CLIAdapter: 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 == "devin": + return f".devin/workflows/trellis-{name}.md" elif self.platform == "kilo": return f".kilocode/workflows/{name}.md" elif self.platform == "copilot": @@ -268,6 +307,10 @@ class CLIAdapter: return f".factory/commands/trellis/{name}.md" elif self.platform == "pi": return f".pi/prompts/trellis-{name}.md" + elif self.platform in ("omp", "grok"): + return f"{self.config_dir_name}/commands/trellis-{name}.md" + elif self.platform == "kimi": + return f".kimi-code/skills/trellis-{name}/SKILL.md" else: return f"{self.config_dir_name}/commands/trellis/{name}.md" @@ -293,7 +336,7 @@ class CLIAdapter: return {} # Gemini CLI doesn't have a non-interactive env var elif self.platform == "antigravity": return {} - elif self.platform == "windsurf": + elif self.platform == "devin": return {} elif self.platform == "qoder": return {} @@ -305,6 +348,14 @@ class CLIAdapter: return {} elif self.platform == "pi": return {} + elif self.platform == "trae": + return {} + elif self.platform == "omp": + return {} + elif self.platform == "grok": + return {} + elif self.platform == "kimi": + return {} else: return {"CLAUDE_NON_INTERACTIVE": "1"} @@ -370,9 +421,9 @@ class CLIAdapter: raise ValueError( "Antigravity workflows are UI slash commands; CLI agent run is not supported." ) - elif self.platform == "windsurf": + elif self.platform == "devin": raise ValueError( - "Windsurf workflows are UI slash commands; CLI agent run is not supported." + "Devin workflows are UI slash commands; CLI agent run is not supported." ) elif self.platform == "qoder": cmd = ["qodercli", "-p", prompt] @@ -390,6 +441,21 @@ class CLIAdapter: ) elif self.platform == "pi": cmd = ["pi", "-p", prompt] + elif self.platform == "trae": + raise ValueError( + "Trae is IDE-only; CLI agent run is not supported." + ) + elif self.platform == "omp": + raise ValueError( + "OMP uses native task tool for agent runs; CLI agent run is not supported." + ) + elif self.platform == "grok": + # Headless single-prompt; sub-agents use in-process spawn_subagent. + cmd = ["grok", "-p", prompt, "--yolo"] + elif self.platform == "kimi": + # Headless single-prompt with auto-approval; sub-agents are the + # built-in coder/explore/plan agents dispatched in-session. + cmd = ["kimi", "-p", prompt, "--yolo"] else: # claude cmd = ["claude", "-p"] @@ -436,9 +502,9 @@ class CLIAdapter: raise ValueError( "Antigravity workflows are UI slash commands; CLI resume is not supported." ) - elif self.platform == "windsurf": + elif self.platform == "devin": raise ValueError( - "Windsurf workflows are UI slash commands; CLI resume is not supported." + "Devin workflows are UI slash commands; CLI resume is not supported." ) elif self.platform == "qoder": return ["qodercli", "--resume", session_id] @@ -456,6 +522,18 @@ class CLIAdapter: ) elif self.platform == "pi": return ["pi", "-c", session_id] + elif self.platform == "trae": + raise ValueError( + "Trae is IDE-only; CLI resume is not supported." + ) + elif self.platform == "omp": + raise ValueError( + "OMP uses native task tool for agent runs; CLI resume is not supported." + ) + elif self.platform == "grok": + return ["grok", "-c"] + elif self.platform == "kimi": + return ["kimi", "--session", session_id] else: return ["claude", "--resume", session_id] @@ -518,8 +596,8 @@ class CLIAdapter: return "gemini" elif self.platform == "antigravity": return "agy" - elif self.platform == "windsurf": - return "windsurf" + elif self.platform == "devin": + return "devin" elif self.platform == "qoder": return "qodercli" elif self.platform == "codebuddy": @@ -530,6 +608,14 @@ class CLIAdapter: return "droid" elif self.platform == "pi": return "pi" + elif self.platform == "trae": + return "trae" + elif self.platform == "omp": + return "omp" + elif self.platform == "grok": + return "grok" + elif self.platform == "kimi": + return "kimi" else: return "claude" @@ -540,7 +626,15 @@ class CLIAdapter: Claude Code, OpenCode, iFlow, and Codex support CLI agent execution. Cursor is IDE-only and doesn't support CLI agents. """ - return self.platform in ("claude", "opencode", "iflow", "codex", "pi") + return self.platform in ( + "claude", + "opencode", + "iflow", + "codex", + "pi", + "grok", + "kimi", + ) @property def requires_agent_definition_file(self) -> bool: @@ -594,14 +688,21 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter: """Get CLI adapter for the specified platform. Args: - platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi') + platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or 'trae') Returns: CLIAdapter instance Raises: ValueError: If platform is not supported + + Note: + 'windsurf' is accepted as a deprecated alias for 'devin' (Windsurf was + renamed to Devin) and normalized before validation. """ + # Deprecated alias: Windsurf was renamed to Devin. + if platform == "windsurf": + platform = "devin" if platform not in ( "claude", "opencode", @@ -612,15 +713,19 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter: "kiro", "gemini", "antigravity", - "windsurf", + "devin", "qoder", "codebuddy", "copilot", "droid", "pi", + "trae", + "omp", + "grok", + "kimi", ): raise ValueError( - f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi')" + f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', 'trae', 'omp', 'grok', or 'kimi')" ) return CLIAdapter(platform=platform) # type: ignore @@ -636,18 +741,23 @@ _ALL_PLATFORM_CONFIG_DIRS = ( ".kiro", ".gemini", ".agent", - ".windsurf", + ".devin", + ".windsurf", # deprecated: pre-rename Devin config dir (still a platform signal) ".qoder", ".codebuddy", ".github/copilot", ".factory", ".pi", + ".trae", + ".omp", + ".grok", + ".kimi-code", ) """Platform-specific config directory names used by detect_platform exclusion checks. `.agents/skills/` is NOT listed here: it is a shared cross-platform layer (written by Codex, also consumed by Amp/Cline/Warp/etc. via the agentskills.io standard), not a single-platform signal. Its presence must not -block detection of Kiro, Antigravity, Windsurf, or other platforms.""" +block detection of Kiro, Antigravity, Devin, or other platforms.""" def _has_other_platform_dir(project_root: Path, exclude: set[str]) -> bool: @@ -667,27 +777,33 @@ def detect_platform(project_root: Path) -> Platform: 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 + 5. .gemini directory exists → gemini + 6. .codex exists and no other platform dirs → codex + 7. .kilocode directory exists → kilo + 8. .kiro/skills exists and no other platform dirs → kiro 9. .agent/workflows exists and no other platform dirs → antigravity - 10. .windsurf/workflows exists and no other platform dirs → windsurf + 10. .devin/workflows (or legacy .windsurf/workflows) exists and no other platform dirs → devin 11. .codebuddy directory exists → codebuddy 12. .qoder directory exists → qoder - 13. .pi directory exists → pi - 14. Default → claude + 13. .github/copilot directory exists → copilot + 14. .factory directory exists → droid + 15. .pi directory exists → pi + 16. .trae directory exists → trae + 17. Default → claude Args: project_root: Project root directory Returns: - Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or default 'claude') + Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', 'trae', or default 'claude') """ import os # Check environment variable first env_platform = os.environ.get("TRELLIS_PLATFORM", "").lower() + # Deprecated alias: Windsurf was renamed to Devin. + if env_platform == "windsurf": + env_platform = "devin" if env_platform in ( "claude", "opencode", @@ -698,12 +814,16 @@ def detect_platform(project_root: Path) -> Platform: "kiro", "gemini", "antigravity", - "windsurf", + "devin", "qoder", "codebuddy", "copilot", "droid", "pi", + "trae", + "omp", + "grok", + "kimi", ): return env_platform # type: ignore @@ -749,13 +869,16 @@ def detect_platform(project_root: Path) -> Platform: ): return "antigravity" - # Check for Windsurf workflow directory only when no other platform config exists + # Check for Devin workflow directory only when no other platform config + # exists. `.windsurf/workflows` is the legacy pre-rename path (still detected + # as devin for back-compat until users migrate via `trellis update --migrate`). if ( - project_root / ".windsurf" / "workflows" - ).is_dir() and not _has_other_platform_dir( - project_root, {".windsurf"} + (project_root / ".devin" / "workflows").is_dir() + or (project_root / ".windsurf" / "workflows").is_dir() + ) and not _has_other_platform_dir( + project_root, {".devin", ".windsurf"} ): - return "windsurf" + return "devin" # Check for .codebuddy directory (CodeBuddy-specific) if (project_root / ".codebuddy").is_dir(): @@ -777,6 +900,22 @@ def detect_platform(project_root: Path) -> Platform: if (project_root / ".pi").is_dir(): return "pi" + # Check for .trae directory (Trae IDE-specific) + if (project_root / ".trae").is_dir(): + return "trae" + + # Check for .omp directory (OMP-specific) + if (project_root / ".omp").is_dir(): + return "omp" + + # Check for .grok directory (Grok Build-specific) + if (project_root / ".grok").is_dir(): + return "grok" + + # Check for .kimi-code directory (Kimi Code-specific) + if (project_root / ".kimi-code").is_dir(): + return "kimi" + # Fallback: checkout only has the Codex shared-skills layer # (.agents/skills/trellis-* dirs) and no explicit platform config dir. # Happens on fresh clones where .codex/ is gitignored/absent but the diff --git a/.trellis/scripts/common/config.py b/.trellis/scripts/common/config.py index 93df643f..99f79b23 100755 --- a/.trellis/scripts/common/config.py +++ b/.trellis/scripts/common/config.py @@ -117,11 +117,12 @@ def _parse_yaml_block( key, _, value = stripped.partition(":") key = key.strip() value = _strip_inline_comment(value).strip() + was_quoted = len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'") value = _unquote(value) current_list = None - if value: - # key: value + if value or was_quoted: + # key: value (an explicit quoted "" is a value, not "no value") target[key] = value i += 1 else: @@ -167,6 +168,7 @@ def _next_content_line(lines: list[str], start: int) -> tuple[int, str]: DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal" DEFAULT_MAX_JOURNAL_LINES = 2000 DEFAULT_SESSION_AUTO_COMMIT = True +DEFAULT_CODEX_DISPATCH_MODE = "auto" CONFIG_FILE = "config.yaml" @@ -243,6 +245,128 @@ def get_session_auto_commit(repo_root: Path | None = None) -> bool: return DEFAULT_SESSION_AUTO_COMMIT +def get_codex_dispatch_mode(repo_root: Path | None = None) -> str: + """Return Codex dispatch mode. + + Default is ``auto``, which dispatches Trellis sub-agents and uses native + context injection with a child-side fallback. ``inline`` is an explicit + opt-out. ``sub-agent`` remains a backwards-compatible alias for ``auto``. + + Invalid explicit configuration falls back to ``inline`` rather than + unexpectedly dispatching a sub-agent. This CLI-facing parser is the only + place that emits a warning for invalid values; hook readers fail safely + without producing per-turn warning noise. + """ + config = _load_config(repo_root) + codex = config.get("codex") + if codex is None: + return DEFAULT_CODEX_DISPATCH_MODE + if not isinstance(codex, dict): + print( + f"[WARN] invalid codex config: {codex!r}; using inline", + file=sys.stderr, + ) + return "inline" + + raw = codex.get("dispatch_mode", DEFAULT_CODEX_DISPATCH_MODE) + mode = str(raw).strip().lower() + if mode in ("auto", "inline"): + return mode + if mode == "sub-agent": + return "auto" + print( + f"[WARN] invalid codex.dispatch_mode value: {raw!r}; using inline", + file=sys.stderr, + ) + return "inline" + + +DEFAULT_CONTEXT_INJECTION_MAX_FILE_BYTES = 32768 +DEFAULT_CONTEXT_INJECTION_MAX_ARTIFACT_BYTES = 65536 +DEFAULT_CONTEXT_INJECTION_MAX_TOTAL_BYTES = 131072 + + +def get_context_injection_limits(repo_root: Path | None = None) -> dict[str, int]: + """Return sub-agent context injection byte limits. + + Reads the ``context_injection:`` section of ``.trellis/config.yaml``: + + context_injection: + max_file_bytes: 32768 + max_artifact_bytes: 65536 + max_total_bytes: 131072 + + ``0`` disables the corresponding limit. Missing keys use their default; + invalid (non-int or negative) values fall back to the default for that + key with a stderr warning. + """ + defaults = { + "max_file_bytes": DEFAULT_CONTEXT_INJECTION_MAX_FILE_BYTES, + "max_artifact_bytes": DEFAULT_CONTEXT_INJECTION_MAX_ARTIFACT_BYTES, + "max_total_bytes": DEFAULT_CONTEXT_INJECTION_MAX_TOTAL_BYTES, + } + + config = _load_config(repo_root) + section = config.get("context_injection") + if not isinstance(section, dict): + return defaults + + result = dict(defaults) + for key, default_value in defaults.items(): + if key not in section: + continue + raw = section[key] + try: + value = int(raw) + except (TypeError, ValueError): + print( + f"[WARN] invalid context_injection.{key} value: {raw!r}; " + f"using default {default_value}", + file=sys.stderr, + ) + continue + if value < 0: + print( + f"[WARN] invalid context_injection.{key} value: {raw!r}; " + f"using default {default_value}", + file=sys.stderr, + ) + continue + result[key] = value + + return result + + +DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD = "no-trellis" + + +def get_prompt_injection_config(repo_root: Path | None = None) -> dict[str, str]: + """Return per-turn prompt injection config. + + Reads the ``prompt_injection:`` section of ``.trellis/config.yaml``: + + prompt_injection: + skip_keyword: "no-trellis" # "" disables the escape hatch entirely + + ``skip_keyword`` is the word-boundary, case-insensitive keyword that, when + present in the user's prompt, makes the per-turn workflow-state injection + emit nothing for that turn. Defaults to ``"no-trellis"``. A non-string + value falls back to the default. + """ + defaults = {"skip_keyword": DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD} + + config = _load_config(repo_root) + section = config.get("prompt_injection") + if not isinstance(section, dict): + return defaults + + result = dict(defaults) + raw = section.get("skip_keyword", DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD) + if isinstance(raw, str): + result["skip_keyword"] = raw + return result + + def get_hooks(event: str, repo_root: Path | None = None) -> list[str]: """Get hook commands for a lifecycle event. diff --git a/.trellis/scripts/common/git.py b/.trellis/scripts/common/git.py index c4bf29f5..ed00a9a2 100755 --- a/.trellis/scripts/common/git.py +++ b/.trellis/scripts/common/git.py @@ -29,3 +29,39 @@ def run_git(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]: return result.returncode, result.stdout, result.stderr except Exception as e: return 1, "", str(e) + + +def resolve_default_branch(repo_root: Path) -> str | None: + """Resolve the repository's default branch (origin/HEAD target). + + Tries the local `refs/remotes/origin/HEAD` symbolic ref first (no + network access), then falls back to `git remote show origin` (which + may hit the network but also repairs a missing/stale symbolic-ref). + Returns None when neither resolves, so callers can fall back to their + own pre-existing behavior. + """ + rc, out, _ = run_git(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd=repo_root) + if rc == 0 and out.strip(): + return out.strip().rsplit("/", 1)[-1] + + rc, out, _ = run_git(["remote", "show", "origin"], cwd=repo_root) + if rc == 0: + for line in out.splitlines(): + line = line.strip() + if line.startswith("HEAD branch:"): + branch = line.split(":", 1)[1].strip() + if branch and branch != "(unknown)": + return branch + + return None + + +def branch_exists_locally(branch: str, repo_root: Path) -> bool: + """Check whether a local branch ref exists in the repository.""" + if not branch: + return False + rc, _, _ = run_git( + ["rev-parse", "--verify", "--quiet", f"refs/heads/{branch}"], + cwd=repo_root, + ) + return rc == 0 diff --git a/.trellis/scripts/common/io.py b/.trellis/scripts/common/io.py index 44288f41..c918c1f0 100755 --- a/.trellis/scripts/common/io.py +++ b/.trellis/scripts/common/io.py @@ -8,6 +8,8 @@ for JSON file operations across all Trellis scripts. from __future__ import annotations import json +import os +import tempfile from pathlib import Path @@ -25,13 +27,35 @@ def read_json(path: Path) -> dict | None: def write_json(path: Path, data: dict) -> bool: """Write dict to JSON file with pretty formatting. + The write is atomic: content goes to a temp file in the same directory + and is then renamed over the target. A crash or Ctrl-C mid-write leaves + the existing file intact rather than truncated, so a corrupted task.json + can never make a task silently vanish from `task.py list`. + Returns True on success, False on error. """ + payload = json.dumps(data, indent=2, ensure_ascii=False) try: - path.write_text( - json.dumps(data, indent=2, ensure_ascii=False), - encoding="utf-8", + fd, tmp = tempfile.mkstemp( + dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp" ) + except OSError: + return False + + try: + try: + f = os.fdopen(fd, "w", encoding="utf-8") + except OSError: + # fdopen never took ownership of fd; close it ourselves. + os.close(fd) + raise + with f: + f.write(payload) + os.replace(tmp, path) return True - except (OSError, IOError): + except OSError: + try: + os.unlink(tmp) + except OSError: + pass return False diff --git a/.trellis/scripts/common/safe_commit.py b/.trellis/scripts/common/safe_commit.py index 4174191b..1a1cba28 100755 --- a/.trellis/scripts/common/safe_commit.py +++ b/.trellis/scripts/common/safe_commit.py @@ -58,7 +58,10 @@ TRELLIS_IGNORED_SUBPATHS = ( ) -def safe_trellis_paths_to_add(repo_root: Path) -> list[str]: +def safe_trellis_paths_to_add( + repo_root: Path, + task_name: str | None = None, +) -> list[str]: """Return the list of repo-relative paths the auto-commit should stage. Only includes paths that exist on disk so callers don't pass non-existent @@ -68,12 +71,23 @@ def safe_trellis_paths_to_add(repo_root: Path) -> list[str]: Included: - .trellis/workspace/<developer>/journal-*.md - .trellis/workspace/<developer>/index.md - - .trellis/tasks/<task-dir>/ (every active task directory) - - .trellis/tasks/archive/ (whole archive subtree, if present) + - .trellis/tasks/<task_name>/ (ONLY the current task dir when + ``task_name`` is passed; plus its archive location if the task + already lives under archive/) Excluded (intentionally — these must not be staged): - .trellis/.backup-*, .trellis/worktrees/, .trellis/.template-hashes.json, .trellis/.runtime/, .trellis/.cache/ + + Scope contract (see #303 / break-loop analysis): when ``task_name`` is + passed, the task segment stages ONLY that task directory — it never walks + ``tasks_dir.iterdir()`` over all active tasks. This mirrors + :func:`safe_archive_paths_to_add` and prevents dirty changes in OTHER + parallel-window task dirs from being bundled into the session auto-commit. + + Backwards-compat: with no ``task_name``, the function walks every active + task directory (+ the archive subtree) the old wide way. New callers + should always pass ``task_name``. """ paths: list[str] = [] @@ -93,20 +107,36 @@ def safe_trellis_paths_to_add(repo_root: Path) -> list[str]: f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/index.md" ) - # Active tasks: each direct child of tasks/ that is a directory and not - # the archive root. The archive subtree is added as a single path below. tasks_dir = repo_root / DIR_WORKFLOW / DIR_TASKS - if tasks_dir.is_dir(): - for child in sorted(tasks_dir.iterdir()): - if not child.is_dir(): - continue - if child.name == DIR_ARCHIVE: - continue - paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child.name}") + if not tasks_dir.is_dir(): + return paths - archive_dir = tasks_dir / DIR_ARCHIVE - if archive_dir.is_dir(): - paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}") + if task_name is not None: + # Narrow scope — ONLY the current task directory (active or archived). + # Never iterdir() all tasks: parallel-window dirty task dirs must not + # leak into the session auto-commit. + active_task = tasks_dir / task_name + if active_task.is_dir(): + paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}") + archived_task = tasks_dir / DIR_ARCHIVE / task_name + if archived_task.is_dir(): + paths.append( + f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{task_name}" + ) + return paths + + # Legacy wide scope (no task_name): each direct child of tasks/ that is a + # directory and not the archive root, plus the whole archive subtree. + for child in sorted(tasks_dir.iterdir()): + if not child.is_dir(): + continue + if child.name == DIR_ARCHIVE: + continue + paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child.name}") + + archive_dir = tasks_dir / DIR_ARCHIVE + if archive_dir.is_dir(): + paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}") return paths diff --git a/.trellis/scripts/common/session_context.py b/.trellis/scripts/common/session_context.py index 772c7aad..eb02369a 100755 --- a/.trellis/scripts/common/session_context.py +++ b/.trellis/scripts/common/session_context.py @@ -412,7 +412,7 @@ def _get_update_hint(repo_root: Path) -> str | None: return ( f"Trellis update available: {current_version} -> {latest_version}, " - f"run npm install -g {_PACKAGE_NAME}@latest" + "run trellis update" ) diff --git a/.trellis/scripts/common/task_context.py b/.trellis/scripts/common/task_context.py index fa884120..7c4db163 100755 --- a/.trellis/scripts/common/task_context.py +++ b/.trellis/scripts/common/task_context.py @@ -10,9 +10,9 @@ Provides: Note: ``cmd_init_context`` was removed in v0.5.0-beta.12. JSONL context files are now seeded at ``task.py create`` time with a self-describing - ``_example`` line; the AI agent curates real entries during Phase 1.3 of - the workflow. See ``.trellis/workflow.md`` Phase 1.3 for the current - instructions. + ``_example`` line; the AI agent curates real entries during planning when + the task needs sub-agent/spec context. See ``.trellis/workflow.md`` for the + current planning artifact contract. """ from __future__ import annotations @@ -21,10 +21,36 @@ import argparse import json from pathlib import Path +from .config import get_context_injection_limits +from .git import branch_exists_locally +from .io import read_json from .log import Colors, colored -from .paths import get_repo_root +from .paths import FILE_TASK_JSON, get_repo_root from .task_utils import resolve_task_dir +# Extensions that look like code rather than spec/research docs. Entries with +# one of these extensions outside .trellis/spec/, docs/docs-site, or the +# task's own directory get a hygiene warning in `task.py validate` — the +# reader is a sub-agent, not a human, so code paths belong in the diff the +# agent reads itself, not in implement.jsonl / check.jsonl. +_CODE_FILE_EXTENSIONS = { + ".ts", + ".tsx", + ".js", + ".jsx", + ".mjs", + ".cjs", + ".py", + ".go", + ".rs", + ".java", + ".rb", + ".c", + ".cc", + ".cpp", + ".h", +} + # ============================================================================= # Command: add-context @@ -97,10 +123,26 @@ def cmd_validate(args: argparse.Namespace) -> int: print(f"Target dir: {target_dir}") print() + # Warn (don't fail validation) when the recorded branch is stale — it + # was likely already merged and deleted (#399 item 2). + task_json_path = target_dir / FILE_TASK_JSON + if task_json_path.is_file(): + task_data = read_json(task_json_path) + stored_branch = task_data.get("branch") if task_data else None + if stored_branch and not branch_exists_locally(stored_branch, repo_root): + print( + colored( + f"Warning: recorded branch '{stored_branch}' no longer exists locally " + "(likely merged and deleted).", + Colors.YELLOW, + ) + ) + print() + total_errors = 0 for jsonl_name in ["implement.jsonl", "check.jsonl"]: jsonl_file = target_dir / jsonl_name - errors = _validate_jsonl(jsonl_file, repo_root) + errors = _validate_jsonl(jsonl_file, repo_root, target_dir) total_errors += errors print() @@ -112,11 +154,33 @@ def cmd_validate(args: argparse.Namespace) -> int: return 1 -def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int: +def _is_exempt_from_code_file_warning(file_path: str, task_rel: str) -> bool: + """Whether a jsonl entry path is exempt from the code-file hygiene warning. + + Exempt: spec docs (``.trellis/spec/``), documentation (``docs``, + ``docs-site``), and the task's own directory (execution plans, generated + artifacts, etc. legitimately live there). + """ + posix_path = file_path.replace("\\", "/").lstrip("/") + exempt_prefixes = (".trellis/spec/", "docs/", "docs-site/") + if posix_path.startswith(exempt_prefixes): + return True + if task_rel and (posix_path == task_rel or posix_path.startswith(f"{task_rel}/")): + return True + return False + + +def _validate_jsonl(jsonl_file: Path, repo_root: Path, task_dir: Path | None = None) -> int: """Validate a single JSONL file. Seed rows (no ``file`` field — typically ``{"_example": "..."}``) are skipped silently; they are self-describing comments, not real entries. + + Beyond hard errors (missing file/dir, invalid JSON), this also prints + non-blocking hygiene warnings (never counted in ``errors``, never change + the exit code): entries that look like code files rather than + spec/research docs, and entries whose file size exceeds the configured + sub-agent context injection cap (``context_injection.max_file_bytes``). """ file_name = jsonl_file.name errors = 0 @@ -125,6 +189,15 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int: print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}") return 0 + task_rel = "" + if task_dir is not None: + try: + task_rel = task_dir.resolve().relative_to(repo_root.resolve()).as_posix() + except ValueError: + task_rel = "" + + max_file_bytes = get_context_injection_limits(repo_root).get("max_file_bytes", 0) + line_num = 0 real_entries = 0 for line in jsonl_file.read_text(encoding="utf-8").splitlines(): @@ -152,10 +225,33 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int: 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 + continue + + if not full_path.is_file(): + print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}") + errors += 1 + continue + + extension = Path(file_path).suffix.lower() + if extension in _CODE_FILE_EXTENSIONS and not _is_exempt_from_code_file_warning( + file_path, task_rel + ): + warning = ( + f"{file_name}:{line_num}: Warning: {file_path} looks like a code file — " + "implement/check.jsonl should reference spec/research docs; " + "agents read code themselves" + ) + print(f" {colored(warning, Colors.YELLOW)}") + + if max_file_bytes: + size = full_path.stat().st_size + if size > max_file_bytes: + warning = ( + f"{file_name}:{line_num}: Warning: {file_path} is {size} bytes, " + f"exceeds context_injection.max_file_bytes ({max_file_bytes}); " + "injection will truncate it" + ) + print(f" {colored(warning, Colors.YELLOW)}") if errors == 0: print(f" {colored(f'{file_name}: ✓ ({real_entries} entries)', Colors.GREEN)}") diff --git a/.trellis/scripts/common/task_store.py b/.trellis/scripts/common/task_store.py index dfcd8503..2cd97899 100755 --- a/.trellis/scripts/common/task_store.py +++ b/.trellis/scripts/common/task_store.py @@ -9,6 +9,7 @@ Provides: 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_set_meta - Set/overwrite a task metadata key cmd_add_subtask - Link child task to parent cmd_remove_subtask - Unlink child task from parent """ @@ -23,13 +24,14 @@ from datetime import datetime from pathlib import Path from .config import ( + get_codex_dispatch_mode, get_packages, get_session_auto_commit, is_monorepo, resolve_package, validate_package, ) -from .git import run_git +from .git import branch_exists_locally, resolve_default_branch, run_git from .io import read_json, write_json from .log import Colors, colored from .paths import ( @@ -50,6 +52,7 @@ from .safe_commit import ( from .task_utils import ( archive_task_complete, find_task_by_name, + is_within_tasks_dir, resolve_task_dir, run_task_hooks, ) @@ -113,13 +116,13 @@ def _repo_relative_path(path: Path, repo_root: Path) -> str: # Config directories of platforms that consume implement.jsonl / check.jsonl. # Keep in sync with src/types/ai-tools.ts AI_TOOLS entries — these are the -# platforms listed in workflow.md's "agent-capable" Skill Routing block -# (Class-1 hook-inject + Class-2 pull-based preludes). Kilo / Antigravity / -# Windsurf are NOT in this list: they do not consume JSONL. +# platforms listed in workflow.md's "agent-capable" Skill Routing block. +# Codex is checked separately because explicit inline mode does not consume +# JSONL. Kilo / Antigravity / Devin are NOT in this list either: they load +# specs through skills instead of JSONL. _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = ( ".claude", ".cursor", - ".codex", ".kiro", ".gemini", ".opencode", @@ -128,7 +131,13 @@ _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = ( ".factory", # Factory Droid ".github/copilot", ".pi", # Pi Agent + ".trae", # Trae IDE + ".omp", # Oh My Pi + ".zcode", # ZCode + ".grok", # Grok Build + ".kimi-code", # Kimi Code ) +_CODEX_CONFIG_DIR = ".codex" _SEED_EXAMPLE = ( "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. " @@ -141,13 +150,16 @@ _SEED_EXAMPLE = ( def _has_subagent_platform(repo_root: Path) -> bool: """Return True if any sub-agent-capable platform is configured. - Detected by probing well-known config directories at the repo root. Used - only to decide whether ``task.py create`` should seed empty - ``implement.jsonl`` / ``check.jsonl`` files. + Detected by probing well-known config directories at the repo root. Codex + counts by default through ``codex.dispatch_mode: auto`` (including the + legacy ``sub-agent`` alias); explicit inline mode loads context through + skills, not JSONL. """ for config_dir in _SUBAGENT_CONFIG_DIRS: if (repo_root / config_dir).is_dir(): return True + if (repo_root / _CODEX_CONFIG_DIR).is_dir(): + return get_codex_dispatch_mode(repo_root) == "auto" return False @@ -162,6 +174,52 @@ def _write_seed_jsonl(path: Path) -> None: path.write_text(json.dumps(seed, ensure_ascii=False) + "\n", encoding="utf-8") +def _parse_meta_pairs(pairs: list[str] | None) -> dict[str, str] | None: + """Parse repeatable ``--meta key=value`` pairs into a dict. + + Returns ``None`` (after printing an error naming the bad value) on the + first malformed pair: missing ``=`` or an empty key. Values are stored + as-is (strings, no nesting, no type coercion). + """ + meta: dict[str, str] = {} + for pair in pairs or []: + key, sep, value = pair.partition("=") + if not sep or not key: + print( + colored(f"Error: malformed --meta value '{pair}' (expected key=value)", Colors.RED), + file=sys.stderr, + ) + return None + meta[key] = value + return meta + + +def _default_prd_content(title: str, description: str | None = None) -> str: + """Return the default PRD skeleton created with every task.""" + goal = (description or "").strip() or "TBD." + heading = title.strip() or "Untitled task" + return f"""# {heading} + +## Goal + +{goal} + +## Requirements + +- TBD + +## Acceptance Criteria + +- [ ] TBD + +## Notes + +- Keep `prd.md` focused on requirements, constraints, and acceptance criteria. +- Lightweight tasks can remain PRD-only. +- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`. +""" + + # ============================================================================= # Command: create # ============================================================================= @@ -174,6 +232,11 @@ def cmd_create(args: argparse.Namespace) -> int: print(colored("Error: title is required", Colors.RED), file=sys.stderr) return 1 + # Validate --meta (CLI source: fail-fast, before any directory is created) + meta = _parse_meta_pairs(getattr(args, "meta", None)) + if meta is None: + return 1 + # Validate --package (CLI source: fail-fast) package: str | None = getattr(args, "package", None) if not is_monorepo(repo_root): @@ -213,6 +276,34 @@ def cmd_create(args: argparse.Namespace) -> int: # Create task directory with MM-DD-slug format tasks_dir = get_tasks_dir(repo_root) date_prefix = generate_task_date_prefix() + + # Guard against date-prefixed --slug (e.g. a full task dir name pasted in), + # which would otherwise produce MM-DD-MM-DD-slug (issue #377). Only an + # explicit --slug is guarded; title-derived slugs are left untouched. + if args.slug: + m = re.match(r"^(\d{2})-(\d{2})-(.+)$", slug) + if m and 1 <= int(m.group(1)) <= 12 and 1 <= int(m.group(2)) <= 31: + slug_prefix = f"{m.group(1)}-{m.group(2)}" + if slug_prefix == date_prefix: + slug = m.group(3) + print( + colored( + f'warning: --slug should not include the MM-DD prefix; normalized to "{slug}"', + Colors.YELLOW, + ), + file=sys.stderr, + ) + else: + print( + colored( + f"Error: --slug starts with a date prefix ({slug_prefix}-), but task.py create always uses today's date ({date_prefix}).", + Colors.RED, + ), + file=sys.stderr, + ) + print(f"Pass only the slug body, e.g. --slug {m.group(3)}", file=sys.stderr) + return 1 + dir_name = f"{date_prefix}-{slug}" task_dir = tasks_dir / dir_name task_json_path = task_dir / FILE_TASK_JSON @@ -231,15 +322,48 @@ def cmd_create(args: argparse.Namespace) -> int: today = datetime.now().strftime("%Y-%m-%d") - # Record current branch as base_branch (PR target) + # Record the PR target branch. Prefer the repo's actual default branch + # (origin/HEAD) so creating a task from a feature branch doesn't + # mis-stamp that feature branch as the PR target (#399 item 1). Falls + # back to the checked-out branch when the default can't be resolved + # (no remote configured, offline, etc.) — the pre-existing behavior. + # --base-branch lets the caller override both when neither is correct. _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root) current_branch = branch_out.strip() or "main" + explicit_base_branch: str | None = getattr(args, "base_branch", None) + if explicit_base_branch: + base_branch = explicit_base_branch + else: + resolved_base_branch = resolve_default_branch(repo_root) + if resolved_base_branch: + base_branch = resolved_base_branch + else: + base_branch = current_branch + print( + colored( + f"warning: could not resolve the repository's default branch " + f"(no remote configured, offline, etc.); stamping base_branch as " + f"the checked-out branch '{base_branch}'. Pass --base-branch to override.", + Colors.YELLOW, + ), + file=sys.stderr, + ) + + description = (args.description or "").strip() + if not description.strip(): + print( + colored( + "warning: task description is empty; pass --description to improve search and later audits.", + Colors.YELLOW, + ), + file=sys.stderr, + ) task_data = { "id": slug, "name": slug, "title": args.title, - "description": args.description or "", + "description": description, "status": "planning", "dev_type": None, "scope": None, @@ -250,7 +374,7 @@ def cmd_create(args: argparse.Namespace) -> int: "createdAt": today, "completedAt": None, "branch": None, - "base_branch": current_branch, + "base_branch": base_branch, "worktree_path": None, "commit": None, "pr_url": None, @@ -259,14 +383,21 @@ def cmd_create(args: argparse.Namespace) -> int: "parent": None, "relatedFiles": [], "notes": "", - "meta": {}, + "meta": meta, } write_json(task_json_path, task_data) + prd_path = task_dir / "prd.md" + if not prd_path.exists(): + prd_path.write_text( + _default_prd_content(args.title, description), + encoding="utf-8", + ) + # Seed implement.jsonl / check.jsonl for sub-agent-capable platforms. - # Agent curates real entries in Phase 1.3 (see .trellis/workflow.md). - # Agent-less platforms (Kilo / Antigravity / Windsurf) skip this — they + # Agent curates real entries during planning when the task needs them. + # Agent-less platforms (Kilo / Antigravity / Devin) skip this — they # load specs via the trellis-before-dev skill instead of JSONL. seeded_jsonl = False if _has_subagent_platform(repo_root): @@ -303,30 +434,70 @@ def cmd_create(args: argparse.Namespace) -> int: # outside an AI session) — the task is still created, the user can run # task.py start later. Pointer is session-scoped so this never affects # other AI sessions. - try: - from .active_task import resolve_context_key, set_active_task - if resolve_context_key(): + if getattr(args, "no_start", False): + print( + colored( + "Skipped session activation (--no-start); run task.py start when ready.", + Colors.YELLOW, + ), + file=sys.stderr, + ) + else: + try: + from .active_task import resolve_context_key, set_active_task + except Exception as exc: + print( + colored(f"Warning: session activation unavailable (import failed: {exc})", Colors.YELLOW), + file=sys.stderr, + ) + else: try: - rel_dir = task_dir.relative_to(repo_root).as_posix() - except ValueError: - rel_dir = str(task_dir) - set_active_task(rel_dir, repo_root) - except Exception: - pass + context_key = resolve_context_key() + except Exception as exc: + print( + colored(f"Warning: session activation failed (context resolution: {exc})", Colors.YELLOW), + file=sys.stderr, + ) + else: + # No session identity is the normal CLI-outside-an-AI-session + # case (see comment above) — stay silent, not a failure. + if context_key: + try: + rel_dir = task_dir.relative_to(repo_root).as_posix() + except ValueError: + rel_dir = str(task_dir) + try: + active = set_active_task(rel_dir, repo_root) + except Exception as exc: + print( + colored(f"Warning: session activation failed (pointer persistence: {exc})", Colors.YELLOW), + file=sys.stderr, + ) + else: + if active: + print( + colored(f"Activated task for this session: {active.task_path}", Colors.GREEN), + file=sys.stderr, + ) + print(f"Source: {active.source}", file=sys.stderr) + else: + print( + colored("Warning: session activation failed (no pointer returned)", Colors.YELLOW), + 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(" - Fill prd.md with requirements and acceptance criteria", file=sys.stderr) + print(" - Lightweight task: PRD-only is valid", file=sys.stderr) + print(" - Complex task: add design.md and implement.md before task.py start", file=sys.stderr) if seeded_jsonl: print( - " 2. Curate implement.jsonl / check.jsonl (spec + research files only — " - "see .trellis/workflow.md Phase 1.3)", + " - Curate implement.jsonl / check.jsonl as spec/research manifests when sub-agents need context", file=sys.stderr, ) - print(" 3. Run: python3 task.py start <dir>", file=sys.stderr) - else: - print(" 2. Run: python3 task.py start <dir>", file=sys.stderr) + print(" - Use /trellis:continue or phase context to decide the next step", file=sys.stderr) print("", file=sys.stderr) # Output relative path for script chaining @@ -363,6 +534,17 @@ def cmd_archive(args: argparse.Namespace) -> int: print(f" - {t.dir_name}/", file=sys.stderr) return 1 + # Refuse to archive anything that isn't a real task directly under + # .trellis/tasks/. A mistyped name (e.g. "src") resolves to repo_root/src, + # which is a dir but not a task — without this guard archive would move the + # user's source directory out of the repo. + if not is_within_tasks_dir(task_dir, repo_root): + print(colored( + f"Error: refusing to archive '{task_name}': " + f"{task_dir} is not a task under {tasks_dir}", + Colors.RED), file=sys.stderr) + return 1 + dir_name = task_dir.name task_json_path = task_dir / FILE_TASK_JSON @@ -374,6 +556,19 @@ def cmd_archive(args: argparse.Namespace) -> int: if task_json_path.is_file(): data = read_json(task_json_path) if data: + # Warn (don't block) when the recorded branch is stale — it was + # likely already merged and deleted (#399 item 2). + stored_branch = data.get("branch") + if stored_branch and not branch_exists_locally(stored_branch, repo_root): + print( + colored( + f"Warning: recorded branch '{stored_branch}' no longer exists locally " + "(likely merged and deleted).", + Colors.YELLOW, + ), + file=sys.stderr, + ) + data["status"] = "completed" data["completedAt"] = today write_json(task_json_path, data) @@ -410,7 +605,16 @@ def cmd_archive(args: argparse.Namespace) -> int: # Auto-commit unless --no-commit if not getattr(args, "no_commit", False): - _auto_commit_archive(dir_name, repo_root, modified_children) + if not _auto_commit_archive(dir_name, repo_root, modified_children): + print( + colored( + "Archive moved on disk, but git auto-commit did not complete. " + "Resolve `git status` before continuing.", + Colors.RED, + ), + file=sys.stderr, + ) + return 1 # Return the archive path print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}") @@ -427,7 +631,7 @@ def _auto_commit_archive( task_name: str, repo_root: Path, modified_children: list[str] | None = None, -) -> None: +) -> bool: """Stage Trellis-owned task paths and commit after archive. Scoped narrowly to the archived task's source + destination paths @@ -449,14 +653,21 @@ def _auto_commit_archive( "[OK] session_auto_commit: false — skipping git stage/commit.", file=sys.stderr, ) - return + return True + + source_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}" + rc, tracked_out, _ = run_git( + ["ls-files", "--", source_rel], + cwd=repo_root, + ) + source_was_tracked = rc == 0 and bool(tracked_out.strip()) paths = safe_archive_paths_to_add( repo_root, task_name=task_name, modified_children=modified_children ) if not paths: print("[OK] No task changes to commit.", file=sys.stderr) - return + return True success, _, err = safe_git_add(paths, repo_root) if not success: @@ -467,7 +678,7 @@ def _auto_commit_archive( f"[WARN] git add failed: {err.strip() if err else 'unknown error'}", file=sys.stderr, ) - return + return not source_was_tracked # Belt-and-suspenders for the phantom-delete bug: `safe_git_add` uses # `git add` (no -A) which only stages additions/modifications. The @@ -478,7 +689,6 @@ def _auto_commit_archive( # # `--ignore-unmatch` makes this a no-op when the task was never tracked # (e.g. archiving a task that lived only in working tree). - source_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}" run_git( ["rm", "-r", "--cached", "--ignore-unmatch", "--", source_rel], cwd=repo_root, @@ -490,14 +700,16 @@ def _auto_commit_archive( ) if rc == 0: print("[OK] No task changes to commit.", file=sys.stderr) - return + return True 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) + return True else: print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr) + return not source_was_tracked # ============================================================================= @@ -695,3 +907,39 @@ def cmd_set_scope(args: argparse.Namespace) -> int: print(colored(f"✓ Scope set to: {scope}", Colors.GREEN)) return 0 + + +# ============================================================================= +# Command: set-meta +# ============================================================================= + +def cmd_set_meta(args: argparse.Namespace) -> int: + """Set/overwrite one metadata key on an existing task.""" + repo_root = get_repo_root() + target_dir = resolve_task_dir(args.dir, repo_root) + key = args.key + value = args.value + + if not key: + print(colored("Error: Missing arguments", Colors.RED)) + print("Usage: python3 task.py set-meta <task-dir> <key> <value>") + 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 + + meta = data.get("meta") + if not isinstance(meta, dict): + meta = {} + meta[key] = value + data["meta"] = meta + write_json(task_json, data) + + print(colored(f"✓ Meta set: {key} = {value}", Colors.GREEN)) + return 0 diff --git a/.trellis/scripts/common/task_utils.py b/.trellis/scripts/common/task_utils.py index 62c215e3..62368db0 100755 --- a/.trellis/scripts/common/task_utils.py +++ b/.trellis/scripts/common/task_utils.py @@ -69,6 +69,30 @@ def is_safe_task_path(task_path: str, repo_root: Path | None = None) -> bool: return True +def is_within_tasks_dir(task_dir_abs: Path, repo_root: Path | None = None) -> bool: + """Check that a resolved task directory really is a task under the tasks dir. + + A real task lives directly at ``.trellis/tasks/<name>``. This returns True + only when ``task_dir_abs`` is an immediate child of the tasks directory. + + Guards archive: ``resolve_task_dir`` falls back to ``repo_root/<name>`` for + an unknown name, so a mistyped ``task.py archive src`` resolves to the real + ``src/`` source directory. Without this check archive would ``shutil.move`` + it out of the repo. Also rejects the tasks dir itself and anything nested + under ``archive/`` (already-archived tasks). + """ + if repo_root is None: + repo_root = get_repo_root() + try: + resolved = task_dir_abs.resolve() + tasks_resolved = get_tasks_dir(repo_root).resolve() + except (OSError, RuntimeError): + return False + if resolved.parent != tasks_resolved: + return False + return resolved.name != "archive" + + # ============================================================================= # Task Lookup # ============================================================================= diff --git a/.trellis/scripts/common/trellis_config.py b/.trellis/scripts/common/trellis_config.py index 5dbec7a0..a4a6d1e8 100755 --- a/.trellis/scripts/common/trellis_config.py +++ b/.trellis/scripts/common/trellis_config.py @@ -78,10 +78,11 @@ def _parse_yaml_block( key, _, value = stripped.partition(":") key = key.strip() value = _strip_inline_comment(value).strip() + was_quoted = len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'") value = _unquote(value) current_list = None - if value: + if value or was_quoted: target[key] = value i += 1 else: diff --git a/.trellis/scripts/common/workflow_phase.py b/.trellis/scripts/common/workflow_phase.py index 2b4acd0f..9e1c619c 100755 --- a/.trellis/scripts/common/workflow_phase.py +++ b/.trellis/scripts/common/workflow_phase.py @@ -60,15 +60,12 @@ def _parse_marker(line: str) -> tuple[bool, list[str]] | None: def get_phase_index() -> str: - """Return Phase Index + Phase 1/2/3 step bodies from workflow.md. + """Return the compact Phase Index summary from workflow.md. - Matches what the SessionStart hook injects into the `<workflow>` block: - starts at `## Phase Index`, continues through `## Phase 1: Plan`, - `## Phase 2: Execute`, `## Phase 3: Finish`, stops at - `## Customizing Trellis (for forks)` (the docs-for-forks footer). - `[workflow-state:STATUS]` tag blocks (now embedded in Phase Index since - v0.5.0-rc.0) are consumed by the UserPromptSubmit hook so they're - stripped from this output. + SessionStart and no-step phase context use this small summary as their + orientation payload. Detailed Phase 1/2/3 instructions are loaded with + ``get_step`` on demand. ``[workflow-state:STATUS]`` tag blocks are + consumed by the per-turn hook, so they're stripped from this output. """ text = _read_workflow() lines = text.splitlines() @@ -80,7 +77,7 @@ def get_phase_index() -> str: if start is None and stripped == _PHASE_INDEX_HEADING: start = i continue - if start is not None and stripped == "## Customizing Trellis (for forks)": + if start is not None and stripped == "## Phase 1: Plan": end = i break @@ -147,27 +144,34 @@ def _platform_matches(platform: str, block_names: list[str]) -> bool: def resolve_effective_platform(platform: str, config: dict) -> str: """Map ``codex`` to a dispatch-mode-namespaced virtual platform name. - When ``--platform codex`` is passed, return ``"codex-inline"`` (default) - or ``"codex-sub-agent"`` based on ``.trellis/config.yaml`` ``codex.dispatch_mode``. + When ``--platform codex`` is passed, return ``"codex-sub-agent"`` by + default or ``"codex-inline"`` when explicitly configured in + ``.trellis/config.yaml``. ``sub-agent`` remains an alias for ``auto``. ``filter_platform`` then surfaces blocks whose marker lists include the namespaced name (e.g. ``[codex-sub-agent, ...]`` or ``[codex-inline, Kilo, - Antigravity, Windsurf]``). + Antigravity, Devin]``). - Default is ``inline`` because Codex sub-agents run with ``fork_turns="none"`` - isolation and can't inherit the parent session's task context — inline - keeps the main agent in charge so context isn't lost. Invalid / missing - values also fall back to inline. + Native Codex context injection supports the ``auto`` default. Invalid + explicit values fall back to ``inline`` safely; this renderer deliberately + does not warn because it can run in normal CLI output flows. Other platforms are returned unchanged. """ if platform == "codex": - mode = "inline" + mode = "auto" codex_cfg = config.get("codex") if isinstance(config, dict) else None - if isinstance(codex_cfg, dict): - cfg_mode = codex_cfg.get("dispatch_mode") - if cfg_mode in ("inline", "sub-agent"): - mode = cfg_mode - return f"codex-{mode}" + if codex_cfg is not None: + if not isinstance(codex_cfg, dict): + mode = "inline" + else: + cfg_mode = str(codex_cfg.get("dispatch_mode", mode)).strip().lower() + if cfg_mode == "inline": + mode = "inline" + elif cfg_mode in ("auto", "sub-agent"): + mode = "auto" + else: + mode = "inline" + return "codex-sub-agent" if mode == "auto" else "codex-inline" return platform diff --git a/.trellis/scripts/task.py b/.trellis/scripts/task.py index a3493bd6..7e82eacb 100755 --- a/.trellis/scripts/task.py +++ b/.trellis/scripts/task.py @@ -4,16 +4,17 @@ Task Management Script. Usage: - python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>] + python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>] [--no-start] 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 active task - python3 task.py current [--source] # Show active task + python3 task.py current [--source] [--json] # Show active task python3 task.py finish # Clear active 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 set-meta <dir> <key> <value> # Set a task metadata key python3 task.py archive <task-dir> # Archive completed task python3 task.py list # List active tasks python3 task.py list-archive [month] # List archived tasks @@ -24,6 +25,7 @@ Usage: from __future__ import annotations import argparse +import json import sys from common.log import Colors, colored @@ -53,6 +55,7 @@ from common.task_store import ( cmd_set_branch, cmd_set_base_branch, cmd_set_scope, + cmd_set_meta, cmd_add_subtask, cmd_remove_subtask, ) @@ -166,6 +169,27 @@ def cmd_current(args: argparse.Namespace) -> int: repo_root = get_repo_root() active = resolve_active_task(repo_root) + if getattr(args, "json", False): + task_obj = None + if active.task_path: + data = read_json(repo_root / active.task_path / FILE_TASK_JSON) or {} + task_obj = { + "dir": active.task_path, + "id": data.get("id") or data.get("name"), + "title": data.get("title"), + "status": data.get("status"), + "parent": data.get("parent"), + "children": data.get("children", []), + "branch": data.get("branch"), + "base_branch": data.get("base_branch"), + } + print(json.dumps({ + "current_task": task_obj, + "source": active.source, + "stale": active.stale, + }, ensure_ascii=False)) + return 0 if active.task_path else 1 + if args.source: print(f"Current task: {active.task_path or '(none)'}") print(f"Source: {active.source}") @@ -184,6 +208,24 @@ def cmd_current(args: argparse.Namespace) -> int: # Command: list # ============================================================================= +def _display_status(t, all_statuses: dict) -> str: + """Return the status label to show for a task in `list` output. + + A parent task's stored status stays "planning" until someone runs + `task.py start` on the parent directly, even while its children are + actively being worked — a misleading label for anyone scanning the + list (#399 item 3). Show "active" instead when at least one child is + past planning; the stored status.json value is left untouched. + """ + if t.status == "planning" and t.children: + child_in_flight = any( + all_statuses.get(c) not in (None, "planning") for c in t.children + ) + if child_in_flight: + return "active" + return t.status + + def cmd_list(args: argparse.Namespace) -> int: """List active tasks.""" repo_root = get_repo_root() @@ -192,6 +234,38 @@ def cmd_list(args: argparse.Namespace) -> int: developer = get_developer(repo_root) filter_mine = args.mine filter_status = args.status + as_json = getattr(args, "json", False) + + # 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()} + + if as_json: + if filter_mine and not developer: + print(json.dumps({"error": "No developer set"}), file=sys.stderr) + return 1 + + items = [] + for dir_name in sorted(all_tasks.keys()): + t = all_tasks[dir_name] + if filter_mine and (t.assignee or "-") != developer: + continue + if filter_status and t.status != filter_status: + continue + items.append({ + "dir": f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}", + "id": t.raw.get("id") or dir_name, + "title": t.title, + "status": t.status, + "display_status": _display_status(t, all_statuses), + "priority": t.priority, + "assignee": t.assignee or None, + "parent": t.parent, + "children": list(t.children), + "package": t.package, + }) + print(json.dumps({"tasks": items}, ensure_ascii=False)) + return 0 if filter_mine: if not developer: @@ -202,10 +276,6 @@ def cmd_list(args: argparse.Namespace) -> int: 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 @@ -228,6 +298,7 @@ def cmd_list(args: argparse.Namespace) -> int: # Children progress progress = children_progress(t.children, all_statuses) + status_label = _display_status(t, all_statuses) # Package tag pkg_tag = f" @{t.package}" if t.package else "" @@ -235,9 +306,9 @@ def cmd_list(args: argparse.Namespace) -> int: prefix = " " * indent + " - " if filter_mine: - print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress}{marker}") + print(f"{prefix}{dir_name}/ ({status_label}){pkg_tag}{progress}{marker}") else: - print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}") + print(f"{prefix}{dir_name}/ ({status_label}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}") count += 1 # Print children indented @@ -245,9 +316,12 @@ def cmd_list(args: argparse.Namespace) -> int: if child_name in all_tasks: _print_task(child_name, indent + 1) - # Display only top-level tasks (those without a parent) + # Display only top-level tasks: those without a parent, plus orphans + # whose recorded parent is not (or no longer) in the active set — a + # dangling parent ref must still render flat instead of disappearing. for dir_name in sorted(all_tasks.keys()): - if not all_tasks[dir_name].parent: + parent = all_tasks[dir_name].parent + if not parent or parent not in all_tasks: _print_task(dir_name) if count == 0: @@ -307,6 +381,7 @@ 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 create <title> --no-start Create without making it active in this session 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 @@ -316,10 +391,11 @@ Usage: 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 set-meta <dir> <key> <value> Set/overwrite a task metadata key python3 task.py archive <task-dir> 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 [--mine] [--status <status>] [--json] List tasks python3 task.py list-archive [YYYY-MM] List archived tasks Monorepo options: @@ -328,10 +404,12 @@ Monorepo options: List options: --mine, -m Show only tasks assigned to current developer --status, -s <s> Filter by status (planning, in_progress, review, completed) + --json Output machine-readable JSON (also available on `current`) 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 "Add login feature" --meta linear=ENG-123 --meta epic=auth python3 task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent python3 task.py add-context <dir> implement .trellis/spec/cli/backend/auth.md "Auth guidelines" python3 task.py set-branch <dir> task/add-login @@ -369,12 +447,12 @@ def main() -> int: file=sys.stderr, ) print( - "sub-agent-capable platforms and curated by the AI during Phase 1.3.", + "sub-agent-capable platforms and curated by the AI during planning when needed.", file=sys.stderr, ) - print("See .trellis/workflow.md Phase 1.3 or run:", file=sys.stderr) + print("See .trellis/workflow.md planning artifact guidance or run:", file=sys.stderr) print( - " python3 ./.trellis/scripts/get_context.py --mode phase --step 1.3", + " python3 ./.trellis/scripts/get_context.py --mode phase --step 1", file=sys.stderr, ) print( @@ -392,12 +470,26 @@ def main() -> int: # 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("--slug", "-s", help="Task slug without the MM-DD date prefix") 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") + p_create.add_argument( + "--base-branch", + help="PR target branch (overrides origin/HEAD detection and the checked-out-branch fallback)", + ) + p_create.add_argument( + "--meta", + action="append", + help="Task metadata key=value (repeatable)", + ) + p_create.add_argument( + "--no-start", + action="store_true", + help="Create the task without making it active in this session", + ) # add-context p_add = subparsers.add_parser("add-context", help="Add context entry") @@ -422,6 +514,8 @@ def main() -> int: p_current = subparsers.add_parser("current", help="Show active task") p_current.add_argument("--source", action="store_true", help="Show active task source") + p_current.add_argument("--json", action="store_true", + help="Output machine-readable JSON") # finish subparsers.add_parser("finish", help="Clear active task") @@ -441,6 +535,12 @@ def main() -> int: p_scope.add_argument("dir", help="Task directory") p_scope.add_argument("scope", help="Scope name") + # set-meta + p_setmeta = subparsers.add_parser("set-meta", help="Set/overwrite a task metadata key") + p_setmeta.add_argument("dir", help="Task directory") + p_setmeta.add_argument("key", help="Metadata key") + p_setmeta.add_argument("value", help="Metadata value") + # archive p_archive = subparsers.add_parser("archive", help="Archive task") p_archive.add_argument("name", help="Task directory or name") @@ -450,6 +550,7 @@ def main() -> int: 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") + p_list.add_argument("--json", action="store_true", help="Output machine-readable JSON") # add-subtask p_addsub = subparsers.add_parser("add-subtask", help="Link child task to parent") @@ -482,6 +583,7 @@ def main() -> int: "set-branch": cmd_set_branch, "set-base-branch": cmd_set_base_branch, "set-scope": cmd_set_scope, + "set-meta": cmd_set_meta, "archive": cmd_archive, "add-subtask": cmd_add_subtask, "remove-subtask": cmd_remove_subtask, diff --git a/.trellis/spec/hyb-oa/project-governance.md b/.trellis/spec/hyb-oa/project-governance.md index d13cf889..cf9a62d7 100644 --- a/.trellis/spec/hyb-oa/project-governance.md +++ b/.trellis/spec/hyb-oa/project-governance.md @@ -51,11 +51,92 @@ rollback and future self-maintenance. ## Current Local Commands +- `python3 -m compileall -q .trellis/scripts .codex/hooks` - `python3 ./.trellis/scripts/task.py validate <task>` - `node scripts/qa/check-trellis-governance.mjs` - `git diff --check` - `bash scripts/check-minimal.sh` +## Scenario: Trellis Project Upgrade Compatibility + +### 1. Scope / Trigger + +- Trigger: upgrading the project Trellis version, applying migrations, or + refreshing managed platform assets. +- Purpose: prevent a template update from silently breaking the project's + actual Python runtime or enabling an AI platform that is not in scope. + +### 2. Signatures + +- Version check: `trellis --version` and `.trellis/.version`. +- Runtime check: + `python3 -m compileall -q .trellis/scripts .codex/hooks`. +- Project preview: `trellis update --dry-run` using the target CLI version. +- Task smoke tests: `task.py validate`, `task.py current --source --json` and + `task.py list --json`. + +### 3. Contracts + +- Run compile checks with the same `python3` executable used by project + Trellis commands; compatibility with a newer developer interpreter is not + sufficient. +- Project-enabled platforms are authoritative. A local-only settings directory + such as `.claude/` must not cause an out-of-scope managed platform bundle to + enter the upgrade diff. +- Temporarily isolating a local-only platform directory is allowed only when + its exact contents are restored after update and the final diff confirms no + managed assets for that platform were added. +- Preserve intentional local overlays through explicit merge; do not use + `--force` to erase `workflow.md`, `config.yaml`, `.gitignore` or `AGENTS.md`. +- Treat dry-run output and manifest cleanliness as evidence, but do not assume + a dry-run is write-free; inspect `git status` before and after it. + +### 4. Validation & Error Matrix + +- Runtime compile failure -> stop the upgrade and fix or revert the incompatible + managed script before running task commands. +- Unexpected platform assets in dry-run or final diff -> stop, correct platform + discovery, and rerun the preview. +- `.new` conflict files remain -> upgrade is incomplete; merge or discard each + one deliberately. +- Post-upgrade dry-run changes manifest again -> investigate template hash drift + before commit. +- Only documented project overlays remain modified -> acceptable after task and + governance gates pass. + +### 5. Good/Base/Bad Cases + +- Good: isolate a local-only `.claude/`, update Codex assets, restore the local + setting, compile with project Python, then verify a stable dry-run. +- Base: update a project with no local platform ambiguity and run the same + compile, task and governance checks. +- Bad: accept generated Claude assets because `.claude/settings.local.json` + happened to exist, or validate Python code only with a newer interpreter. + +### 6. Tests Required + +- Assert CLI and project versions match the intended target. +- Compile every Python file under `.trellis/scripts` and `.codex/hooks`. +- Exercise task validate/current/list and run Trellis governance. +- Confirm no `.new`, backup, bytecode or out-of-scope platform assets are + tracked; finish with `git diff --check`. + +### 7. Wrong vs Correct + +#### Wrong + +```text +trellis update reports success -> commit immediately +``` + +#### Correct + +```text +preview platform scope -> conservative update/migrate -> merge overlays +-> compile with project Python -> smoke-test task runtime -> stable dry-run +-> governance and project gates -> commit +``` + ## Legacy Issue CSV - Do not delete, bulk rewrite or move historical `issues/*.csv` files. diff --git a/.trellis/tasks/07-26-trellis-upgrade-project-audit/check.jsonl b/.trellis/tasks/07-26-trellis-upgrade-project-audit/check.jsonl new file mode 100644 index 00000000..9dd3234a --- /dev/null +++ b/.trellis/tasks/07-26-trellis-upgrade-project-audit/check.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/07-26-trellis-upgrade-project-audit/design.md b/.trellis/tasks/07-26-trellis-upgrade-project-audit/design.md new file mode 100644 index 00000000..08f0c456 --- /dev/null +++ b/.trellis/tasks/07-26-trellis-upgrade-project-audit/design.md @@ -0,0 +1,36 @@ +# Trellis 升级与项目状态多维审查设计 + +## 决策门 + +本任务先完成只读盘点和升级方案。用户批准前,不执行 CLI 安装、`trellis update`、模板覆盖、文件删除或业务代码修改。 + +## 升级顺序 + +1. 将全局 Trellis CLI 从当前版本对齐到 npm `latest`,再次 dry-run,避免用旧 CLI 写入旧模板。 +2. 为当前干净基线保留可回滚提交点;注意 0.6.9 的 `trellis update --dry-run` 会实际清理 `.template-hashes.json` 中旧拼写 skill 的 5 个 manifest 条目,不能把 dry-run 当作零写入操作。 +3. 临时安全保存项目内仅含本地设置的 `.claude/` 目录后再升级,避免 Trellis 仅凭目录存在误判 Claude 已启用并生成整套 Claude 资产;升级完成后原样恢复本地设置。 +4. 项目升级优先使用“为冲突文件生成 `.new`、自动更新未改模板”的保守策略并应用 migration;不使用 `--force`。 +5. 对 `AGENTS.md`、`.trellis/workflow.md`、`.trellis/config.yaml`、`.trellis/.gitignore` 逐文件三方合并,保留项目的中文、Trellis-first、Codex inline、禁止自动提交和授权边界。 +6. 应用明确需要的 migration,验证 task、hook、context、governance 和完整本地质量门禁。 + +## 审查维度 + +- 产品/交付状态:继承并复核 07-16 审查与 07-18 本地候选证据,严格区分本地、部署、外部验证。 +- 工程质量:代码规模、测试入口、全量门禁、风险覆盖和依赖/构建观察项。 +- Trellis 治理:版本漂移、模板定制、task/spec/archive、Codex 集成和 session 状态。 +- 文档治理:事实源、未注册文档、历史 Issue 与重复状态描述。 +- 仓库卫生:ignored 生成物、tracked 历史证据、体积热点和清理验证。 +- 运维边界:部署、生产配置、真实数据、企微和付款仍需单独授权。 + +## 建议分级 + +- P0:会导致升级覆盖、本地候选状态误判或高风险开放项失管的问题。 +- P1:近期应完成、收益明确且可独立验收的改进。 +- P2:持续治理或触达即修,不打包成巨型重构。 +- 精简项:删除或合并前必须证明非事实源、非审计合同且可从 git 恢复。 + +## 回滚 + +- 全局 CLI 可按升级前版本号重新安装。 +- 项目文件只在干净基线后修改;升级提交与后续优化提交分开,可独立回退。 +- 不手改 `.trellis/.template-hashes.json`;发现 manifest 异常时停止升级并从基线恢复升级提交。 diff --git a/.trellis/tasks/07-26-trellis-upgrade-project-audit/implement.jsonl b/.trellis/tasks/07-26-trellis-upgrade-project-audit/implement.jsonl new file mode 100644 index 00000000..9dd3234a --- /dev/null +++ b/.trellis/tasks/07-26-trellis-upgrade-project-audit/implement.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/07-26-trellis-upgrade-project-audit/implement.md b/.trellis/tasks/07-26-trellis-upgrade-project-audit/implement.md new file mode 100644 index 00000000..2c9e56f7 --- /dev/null +++ b/.trellis/tasks/07-26-trellis-upgrade-project-audit/implement.md @@ -0,0 +1,41 @@ +# Trellis 升级实施清单 + +## 执行步骤 + +- [x] 将全局 Trellis CLI 升级到 0.6.9。 +- [x] 临时隔离并恢复 `.claude/settings.local.json`,避免 Claude 平台误检测。 +- [x] 以 `--create-new --migrate` 升级项目 Trellis 模板和 runtime。 +- [x] 保留 `session_auto_commit: false` 与 `codex.dispatch_mode: inline`。 +- [x] 合并上游 workflow,并恢复 hyb-oa Trellis-first overlay。 +- [x] 保留项目 `.trellis/.gitignore` 的本地 workspace 排除规则。 +- [x] 保留项目 `AGENTS.md`,不追加重复的通用英文托管块。 +- [x] 删除冲突 `.new` 副本和迁移遗留的旧拼写 skill。 +- [x] 运行升级后 task/context/hooks/governance 验证。 +- [x] 运行完整本地质量门禁并记录结果。 +- [x] 评估并更新稳定 spec。 +- [x] 形成中文提交。 +- [ ] 完成 Trellis task 归档。 + +## 验证结果 + +- Trellis CLI、项目版本与 npm latest 均为 0.6.9;升级后 dry-run 稳定。 +- Python 3.9.2 compileall、task validate/current/list 与 Trellis runtime 基本命令通过。 +- `bash scripts/check-minimal.sh` 退出码为 0:Prisma validate、服务端 985/985、前端 typecheck/build、功能矩阵均通过。 +- 非阻断提示仅有既有 `punycode` deprecated 与 Browserslist 数据陈旧提示。 + +## 验证命令 + +- `trellis --version` +- `python3 ./.trellis/scripts/get_context.py` +- `python3 ./.trellis/scripts/task.py validate .trellis/tasks/07-26-trellis-upgrade-project-audit` +- `node scripts/qa/check-trellis-governance.mjs` +- `node scripts/qa/audit-docs.mjs --check-registry` +- `node scripts/qa/check-process-contract.mjs --baseline --json` +- `bash scripts/check-minimal.sh` +- `git diff --check` + +## 回滚点 + +- CLI:重新安装升级前的 0.6.8。 +- 项目:回退本任务升级提交;`.trellis/.backup-*` 只用于本地临时核对,不纳入提交。 +- 若 hooks 或 task runtime 验证失败,停止后续业务工作,不以旧 workflow 混用新 runtime。 diff --git a/.trellis/tasks/07-26-trellis-upgrade-project-audit/prd.md b/.trellis/tasks/07-26-trellis-upgrade-project-audit/prd.md new file mode 100644 index 00000000..bad3e556 --- /dev/null +++ b/.trellis/tasks/07-26-trellis-upgrade-project-audit/prd.md @@ -0,0 +1,88 @@ +# Trellis 升级与项目状态多维审查 + +## Goal + +确认本项目 Trellis 与上游版本差异,制定可回滚的升级方案;同时从任务治理、代码与测试、文档事实源、仓库卫生、交付与运维等角度梳理当前项目状态,输出分级改进、优化和精简建议。 + +## What I already know + +- 当前分支为 `main`,审查开始时工作区干净。 +- 当前没有正式 active task;旧的 `05-31-hyinda-light-ops-greenfield` 仅来自 stale session fallback。 +- 项目只启用 Codex 适配,Trellis 不自动提交。 +- 非微小改动必须先给出方案并等待批准。 +- 近期已经完成过多维系统审查和五人团队正式内用最小收口,需要优先复用结论,避免重复造文档。 + +## Assumptions (temporary) + +- “升级 Trellis”指升级当前项目内由 Trellis 管理的模板、脚本和 Codex 适配文件,不修改全局 npm 包源码。 +- 本阶段只做只读审查、版本比对和方案设计;升级与冲突处理在用户批准后实施。 +- 审查建议应区分立即处理、近期优化和可暂缓项,并明确收益、风险与验证方式。 + +## Open Questions + +- 待版本与模板漂移盘点后,只保留会实质影响升级范围的偏好问题。 + +## Requirements (evolving) + +- 识别已安装 Trellis 版本、项目记录版本和可用最新版。 +- 识别 Trellis 管理文件的本地修改、模板漂移、潜在覆盖冲突和升级回滚点。 +- 复核现有 task、spec、docs、代码结构、测试与质量门禁、仓库产物和交付流程。 +- 复用既有事实源与历史审查,不轻易新建常驻文档。 +- 输出按优先级排序、可执行且避免过度治理的改进建议。 + +## Scope + +- Trellis:全局 CLI 版本、项目模板版本、迁移、Codex hooks/agents/skills、task/spec/workflow/config 兼容性。 +- 工程:服务端、前端、测试与发布门禁的当前可执行状态;本阶段不主动重构业务代码。 +- 治理:task 生命周期、事实源状态、文档注册、历史 Issue 与 Trellis archive 的保留边界。 +- 仓库:已跟踪与本地忽略产物、体积热点、可安全清理项和防复发规则。 +- 交付:本地候选、部署、公网/真机/真实数据等状态边界是否仍准确。 + +## Acceptance Criteria (evolving) + +- [x] 给出 Trellis 当前版本、目标版本、升级命令、冲突预案与验证清单。 +- [x] 给出项目现状摘要,并以证据路径支撑关键结论。 +- [x] 建议覆盖改进、优化、精简三个方向,并区分优先级。 +- [x] 在任何非微小修改前获得用户批准。 +- [x] 获批实施后通过 Trellis 治理、文档注册、核心前后端及 `git diff --check` 等适用验证。 + +## Acceptance Evidence + +- Trellis CLI 与项目模板均已升级到 0.6.9;升级后 dry-run 不再产生 manifest 变化,仅识别五个预期本地定制文件。 +- 项目实际 Python 3.9.2 已通过 `.trellis/scripts` 与 `.codex/hooks` 全量编译检查;并修复 0.6.9 `task_context.py` 的嵌套多行 f-string 兼容问题。 +- Trellis task 的 validate/current/list、governance、process contract、敏感扫描与功能矩阵通过。 +- Prisma validate 通过;服务端 161 个测试文件、985/985 测试通过;前端 typecheck 与 production build 通过。 +- 文档 registry 仅保留既有 22 项假阳性 warning,原因和后续独立治理建议已记录在 research,不作为本次升级失败项。 +- 交付状态继续限定为 `LOCAL_CANDIDATE_PASS`;本任务未部署、未执行公网检查或真实数据操作。 + +## Definition of Done (team quality bar) + +- 适用的 lint、typecheck、测试与流程检查通过。 +- 升级差异经过人工审阅,本地定制不被静默覆盖。 +- 事实源或任务记录按需更新,不制造重复文档。 +- 升级与审查改动形成中文提交并引用任务名。 + +## Test Plan + +- 升级前后分别运行 `trellis update --dry-run`,确认版本、迁移、新增文件、本地修改和用户数据保护行为。 +- 运行 task validate、Trellis governance、文档 registry/process contract、敏感内容扫描和 `git diff --check`。 +- 运行 `bash scripts/check-minimal.sh`,覆盖 Prisma validate、服务端全量 Vitest、前端 typecheck/build 和功能矩阵。 +- 仅在修改业务代码或页面行为时追加聚焦服务端/Playwright;本轮默认不部署、不做公网或真实数据检查。 +- 升级后复核 Codex hooks、active task 解析、`get_context.py` 和 task 生命周期基本命令。 + +## Out of Scope (explicit) + +- 公网部署、生产配置、真实数据写入、数据库重置、企业微信写入和真实付款。 +- 未经批准的大规模业务功能重构。 +- 启用 Codex 之外的 AI 平台适配。 + +## Technical Notes + +- Trellis task:`.trellis/tasks/07-26-trellis-upgrade-project-audit/` +- 工作流:`.trellis/workflow.md` +- Trellis 项目规约:`.trellis/spec/hyb-oa/` +- 关键业务事实源:`docs/文档索引.md`、`docs/文档事实源状态矩阵.md`、`docs/Agent开发交付实践.md` + +## Research References + +- `research/current-state-audit.md`:Trellis 0.6.9 升级预演、当前质量门禁、业务开放项、依赖、文档与仓库卫生审查结论。 diff --git a/.trellis/tasks/07-26-trellis-upgrade-project-audit/research/current-state-audit.md b/.trellis/tasks/07-26-trellis-upgrade-project-audit/research/current-state-audit.md new file mode 100644 index 00000000..f1b12eba --- /dev/null +++ b/.trellis/tasks/07-26-trellis-upgrade-project-audit/research/current-state-audit.md @@ -0,0 +1,95 @@ +# 当前项目状态多维审查 + +## 结论摘要 + +- 当前业务代码仍处于 `LOCAL_CANDIDATE_PASS`,不是已部署或可公网测试;07-18 收口后的业务代码没有新增提交。 +- 本次重新执行完整本地门禁通过:Prisma schema 有效,服务端 161 个测试文件 / 985 项测试全部通过,前端 typecheck 与 production build 通过,流程合同、Trellis governance、敏感扫描和功能矩阵通过。 +- Trellis 项目版本为 0.5.19,本机 CLI 为 0.6.8,npm 最新为 0.6.9。升级应直接以 0.6.9 为目标。 +- 当前最需要优先处理的新增工程风险是前端 React Router 两项 moderate advisory;最重要的既有业务开放项仍是统一导出安全与客户端离线数据生命周期。 + +## Trellis 升级审查 + +### 实施结果(2026-07-26) + +- 全局 CLI 与项目模板已从 0.6.8 / 0.5.19 对齐到 0.6.9,并完成官方 migration。 +- 升级时临时隔离仅含本地设置的 `.claude/`,恢复后未生成 Claude 托管资产;项目继续保持 Codex-only。 +- 四个冲突文件按设计完成合并,保留 Trellis-first overlay、Codex inline、禁止自动提交和 `workspace/` 忽略规则;未保留 `.new` 冲突副本。 +- 0.6.9 runtime 的 `task_context.py` 包含 Python 3.9.2 不接受的嵌套多行 f-string;已改为先构造 warning 再着色,并通过全量 compileall 与 task runtime 验证。 +- 升级后 dry-run 不再改变 manifest,仅报告 `task_context.py`、`config.yaml`、`.gitignore`、`workflow.md`、`AGENTS.md` 五个有意保留的项目定制。 + +### 升级前版本与差异 + +- 项目:0.5.19。 +- 本机 CLI:0.6.8。 +- npm latest:0.6.9。 +- 0.6.9 将更新 Trellis runtime、共享 skills、Codex agents/hooks/config,并新增 `.trellis/agents/`、`trellis-channel`、`trellis-session-insight` 和拼写修正后的 `trellis-spec-bootstrap`。 +- `AGENTS.md`、`.trellis/workflow.md`、`.trellis/config.yaml`、`.trellis/.gitignore` 被识别为本地修改,必须三方合并。 + +### 已确认风险 + +1. 仅因为项目存在 `.claude/settings.local.json`,CLI 就把 Claude 判定为已配置平台,计划新增整套 `.claude/` commands、skills、agents、hooks 与 settings;这与 Codex-only 边界冲突。 +2. 0.6.9 的 `update --dry-run` 会实际清理 `.template-hashes.json` 中旧拼写 `trellis-spec-bootstarp` 的 5 个条目,尽管结尾显示 `No changes made`。预演后的非预期修改已恢复。 +3. 上游建议 `--migrate`,涉及旧拼写目录迁移;不能手工提前删除旧 skill 或强制覆盖本地 workflow。 + +### 推荐升级路径 + +1. 升级全局 CLI 到 0.6.9。 +2. 临时安全保存 `.claude/` 本地目录,使 Trellis 只检测到 Codex。 +3. 在干净提交点上执行保守更新和 migration;冲突文件生成 `.new`,不使用 force。 +4. 逐文件合并四个本地定制文件,保留中文沟通、方案批准、Codex inline、no auto commit、生产授权边界。 +5. 恢复 `.claude/settings.local.json`,确认没有新增 Claude 托管资产。 +6. 验证 context、task lifecycle、hooks、governance、完整本地门禁并单独提交升级。 + +## 代码、测试与依赖 + +- 规模:服务端约 200 个跟踪源文件,前端约 297 个跟踪源文件;服务端 161 个单元测试文件,前端测试目录 81 个文件;Prisma migration 61 项。 +- 全量门禁:985/985 服务端测试通过;前端 typecheck/build 通过。 +- 覆盖率配置能生成报告,但没有风险阈值;既有 `QA-COVERAGE-001` 仍开放。建议针对认证、付款、worker、迁移/恢复入口建立风险型阈值,不追求全仓统一百分比。 +- 前端生产依赖审计报告 2 项 moderate,均来自 React Router 6.30.4;公开修复指向 7.18.1,属于需要专项回归的 major migration。 +- 服务端 `npm audit` 因 npm quick-audit endpoint 返回 invalid package tree 而不可用;`npm ls` 无 problem、`npm ci --dry-run` 可完成,说明更像审计接口/锁树兼容问题,但仍应在依赖维护任务中闭合。 +- 可安全优先升级一批服务端 patch/minor;Vitest、React、Vite、Tailwind、pg-boss 等 major 不应混成一次升级。 +- 构建观察:PWA precache 约 2.1 MiB,扫码 vendor chunk 约 411 KiB,Material Symbols 字体约 479 KiB;目前无行为故障证据,先测真实首屏/弱网再优化。 +- 非阻断工具提示:Node `punycode` 弃用;Browserslist 数据约 6 个月未更新。 + +## 业务与交付状态 + +- 07-18 已本地验证:认证代理信任、附件主动内容、员工敏感字段、员工合同附件、付款状态机/审计、核心长表单、移动考勤、响应式表格、权限显隐与本地发布门禁等。 +- 上述本地修复仍未部署和公网复核;两个原 P0 的 Gate 0 外部验证仍需单独授权。 +- 当前 P1 开放项:统一 CSV/表格导出中和;退出/换账号后的离线数据隔离与清除。 +- P2 持续治理:下载凭证日志面、未执行 ACL 元数据、CSP、防误触、通知/搜索定位、路由元数据、错误恢复、冻结差旅入口、错误文案、移动权限与可访问性。 +- 这些 P2 不应打包成巨型重构,按触达页面或独立小任务收敛。 + +## 文档与治理 + +- 事实源矩阵最近审计为 07-16,已正确声明当前可信基线与 Trellis-first 优先级。 +- `audit-docs --check-registry` 报 22 个未注册文件,但这 22 个文件都在同一文档后续“当前事实源/手册/证据/技术参考”章节列出;脚本只解析首个主表,因此 warning 噪声高。 +- 建议让 registry checker 识别后续分类表,或把 22 项合并进主台账;优先改检查器,避免复制 22 行冗长元数据。 +- 历史 Issue CSV 共 349 个、约 3 MiB;Trellis archive 78 个 task、850 个文件、约 5.7 MiB。体积不大且有审计价值,不建议本轮物理删除。 +- 后续应停止向历史 Issue CSV 增长;Trellis archive 只保留 PRD/设计/关键结论和最小证据,不再重复提交大批 raw log。 + +## 仓库卫生 + +- 整体工作目录约 1.2 GiB,但 Git 仓库仅约 27 MiB;主要是本地依赖与忽略产物,不是 Git 历史膨胀。 +- `server/node_modules` 约 644 MiB,`client/node_modules` 约 253 MiB,属于可重建依赖缓存。 +- ignored 证据/输出中,`output` 约 154 MiB、`artifacts` 约 17 MiB,加上 build/report/coverage 可安全回收约 174 MiB;当前 `.gitignore` 已覆盖这些路径。 +- 建议新增一个显式的本地清理命令,只删除已忽略且可重建的固定路径;默认不删 node_modules,不清理已跟踪 Trellis archive。 + +## 建议优先级 + +### P0 / 本轮升级 + +- 安全升级 Trellis 至 0.6.9,隔离 Claude 误检测、dry-run 写 manifest 和本地四文件合并风险。 +- 保持 `LOCAL_CANDIDATE_PASS` 状态上限;未经授权不部署、不做公网/真实数据验证。 + +### P1 / 下一批独立任务 + +- React Router 安全升级与导航/重定向回归。 +- 统一导出安全 + 客户端离线数据生命周期,沿用既有 WP-5。 +- 消除 document registry 的 22 项假阳性 warning,使门禁重新具备信噪比。 + +### P2 / 维护与精简 + +- 风险型覆盖阈值、服务端 audit 可用性、patch/minor 依赖维护。 +- 本地 ignored artifacts 清理命令和证据保留期限。 +- 按触达修复可访问性、导航元数据、错误恢复等开放项。 +- 真实性能数据证明需要后,再优化扫码 chunk、字体和 PWA precache。 diff --git a/.trellis/tasks/07-26-trellis-upgrade-project-audit/task.json b/.trellis/tasks/07-26-trellis-upgrade-project-audit/task.json new file mode 100644 index 00000000..b30e1c42 --- /dev/null +++ b/.trellis/tasks/07-26-trellis-upgrade-project-audit/task.json @@ -0,0 +1,35 @@ +{ + "id": "trellis-upgrade-project-audit", + "name": "trellis-upgrade-project-audit", + "title": "Trellis 升级与项目状态多维审查", + "description": "安全升级项目 Trellis,并基于现有事实源多维复核项目状态,形成分级优化与精简建议。", + "status": "in_progress", + "dev_type": null, + "scope": "trellis-governance-and-project-audit", + "package": null, + "priority": "P2", + "creator": "shunli", + "assignee": "shunli", + "createdAt": "2026-07-26", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [ + ".trellis/", + ".agents/skills/", + ".codex/", + "AGENTS.md", + "docs/", + "scripts/qa/", + "server/", + "client/" + ], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/.trellis/workflow.md b/.trellis/workflow.md index 03bf6cab..fc54ac02 100644 --- a/.trellis/workflow.md +++ b/.trellis/workflow.md @@ -11,7 +11,7 @@ mandatory prerequisite for new work. Required project sequence for non-trivial work: 1. Create or update a `.trellis/tasks/<task>/` task with `prd.md`, acceptance notes and test plan. -2. Add `design.md` or `implement.md` for complex process, API, migration, permission, deployment or workflow-governance changes. +2. Add `design.md` and `implement.md` for complex process, API, migration, permission, deployment or workflow-governance changes. 3. Read the relevant `.trellis/spec/hyb-oa/` files and project docs referenced there. 4. Implement only after the user has approved the task plan. 5. Run the task's test plan plus the Trellis governance gate. @@ -21,15 +21,10 @@ Required project sequence for non-trivial work: Hard boundaries: - Do not let Trellis replace docs fact sources or production deployment gates. -- Do not create new Issue CSV files unless the user explicitly needs a tabular - external audit packet or legacy compatibility export. -- Do not auto-commit Trellis workspace/session files; `session_auto_commit` is - disabled in `.trellis/config.yaml`. -- Do not write passwords, token, Secret, real invoice originals, contract bodies, - bank materials, full accounts, complete local paths, or raw payloads into - `.trellis/`, docs, reports, or chat. -- In Codex, this project defaults to inline dispatch. Sub-agents are optional and - must receive an explicit active task path if used. +- Do not create new Issue CSV files unless the user explicitly needs a tabular external audit packet or legacy compatibility export. +- Do not auto-commit Trellis workspace/session files; `session_auto_commit` is disabled in `.trellis/config.yaml`. +- Do not write passwords, token, Secret, real invoice originals, contract bodies, bank materials, full accounts, complete local paths, or raw payloads into `.trellis/`, docs, reports, or chat. +- In Codex, this project defaults to inline dispatch. Sub-agents are optional and must receive an explicit active task path if used. --- @@ -70,7 +65,7 @@ python3 ./.trellis/scripts/get_context.py --mode packages # list packages / la ### Task System -Every task has its own directory under `.trellis/tasks/{MM-DD-name}/` holding `prd.md`, `implement.jsonl`, `check.jsonl`, `task.json`, optional `research/`, `info.md`. +Every task has its own directory under `.trellis/tasks/{MM-DD-name}/` holding `task.json`, `prd.md`, optional `design.md`, optional `implement.md`, optional `research/`, and context manifests (`implement.jsonl`, `check.jsonl`) for sub-agent-capable platforms. ```bash # Task lifecycle @@ -84,7 +79,7 @@ python3 ./.trellis/scripts/task.py list-archive # Code-spec context (injected into implement/check agents via JSONL). # `implement.jsonl` / `check.jsonl` are seeded on `task create` for sub-agent-capable -# platforms; the AI curates real spec + research entries during Phase 1.3. +# platforms; the AI curates real spec + research entries during planning when needed. python3 ./.trellis/scripts/task.py add-context <name> <action> <file> <reason> python3 ./.trellis/scripts/task.py list-context <name> [action] python3 ./.trellis/scripts/task.py validate <name> @@ -130,7 +125,7 @@ python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed <!-- WORKFLOW-STATE BREADCRUMB CONTRACT (read this before editing the tag blocks below) - The 4 [workflow-state:STATUS] blocks embedded in the ## Phase Index section + The [workflow-state:STATUS] blocks embedded in the ## Phase Index section below are the SINGLE source of truth for the per-turn `<workflow-state>` breadcrumb that every supported AI platform's UserPromptSubmit hook reads. inject-workflow-state.py (Python platforms) and @@ -145,15 +140,17 @@ python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed Every workflow-walkthrough step marked `[required · once]` must have a matching enforcement line in its phase's [workflow-state:*] block. The breadcrumb is the only per-turn channel; if a mandatory step isn't - mentioned there, the AI silently skips it (Phase 1.3 jsonl curation + mentioned there, the AI silently skips it (Phase 1 planning gate skip and Phase 3.4 commit skip both manifested via this gap). TAG ↔ PHASE scoping: [workflow-state:no_task] → no active task; before Phase 1 [workflow-state:planning] → all of Phase 1 (status='planning') - [workflow-state:in_progress] → Phase 2 + Phase 3.1-3.4 + [workflow-state:planning-inline] → Codex inline variant of Phase 1 + [workflow-state:in_progress] → Phase 2 + Phase 3.2-3.4 (status stays 'in_progress' from task.py start until task.py archive) + [workflow-state:in_progress-inline] → Codex inline variant of Phase 2/3 [workflow-state:completed] → currently DEAD: cmd_archive flips status and moves the dir in the same call, so the resolver loses the @@ -173,45 +170,69 @@ python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed ## Phase Index ``` -Phase 1: Plan → figure out what to do (brainstorm + research → prd.md) -Phase 2: Execute → write code and pass quality checks -Phase 3: Finish → distill lessons + wrap-up +Phase 1: Plan → classify, get task-creation consent, then write planning artifacts +Phase 2: Execute → implement only after task status is in_progress +Phase 3: Finish → verify, update spec, commit, and wrap up ``` +### Request Triage + +- Simple conversation or small task: ask only whether this turn should create a Trellis task. If the user says no, skip Trellis for this session. +- Complex task: ask whether you may create a Trellis task and enter planning. If the user says no, do not do broad inline implementation; explain, clarify scope, or suggest a smaller split. +- User approval to create a task is not approval to start implementation. Planning still happens first. + +### Planning Artifacts + +- `prd.md` — requirements, constraints, and acceptance criteria. Do not put technical design or execution checklists here. +- `design.md` — technical design for complex tasks: boundaries, contracts, data flow, tradeoffs, compatibility, rollout / rollback shape. +- `implement.md` — execution plan for complex tasks: ordered checklist, validation commands, review gates, and rollback points. +- `implement.jsonl` / `check.jsonl` — spec and research manifests for sub-agent context. They do not replace `implement.md`. +- Lightweight tasks may be PRD-only. Complex tasks must have `prd.md`, `design.md`, and `implement.md` before `task.py start`. + +### Parent / Child Task Trees + +Use a parent task when one user request contains several independently verifiable deliverables. The parent task owns the source requirement set, the task map, cross-child acceptance criteria, and final integration review; it normally should not be the implementation target unless it also has direct work. + +Use child tasks for deliverables that can be planned, implemented, checked, and archived independently. Parent/child structure is not a dependency system: if one child must wait for another, write that ordering in the child `prd.md` / `implement.md` and keep each child's acceptance criteria testable. + +Create new children with `task.py create "<title>" --slug <name> --parent <parent-dir>`. Link existing tasks with `task.py add-subtask <parent> <child>`, and unlink mistakes with `task.py remove-subtask <parent> <child>`. + <!-- Per-turn breadcrumb: shown when there is no active task (before Phase 1) --> [workflow-state:no_task] -No active task. **A Direct answer** — pure Q&A / explanation / lookup / chat; no file writes + one-line answer + repo reads ≤ 2 files → AI judges, no override needed. -**B Create a task** — any implementation / code change / build / refactor work. Entry sequence: (1) `python3 ./.trellis/scripts/task.py create "<title>"` to create the task (status=planning, breadcrumb switches to [workflow-state:planning] for brainstorm + jsonl phase guidance) → (2) load `trellis-brainstorm` skill to discuss requirements with the user and iterate on prd.md → (3) once prd is done and jsonl is curated, run `task.py start <task-dir>` to enter [workflow-state:in_progress] for the implementation skeleton. **"It looks small" is NOT grounds for downgrading B to A or C**. -**C Inline change** (per-turn only, escape hatch for B) — the user's CURRENT message MUST contain one of: "skip trellis" / "no task" / "just do it" / "don't create a task" / "跳过 trellis" / "别走流程" / "小修一下" / "直接改" / "先别建任务" → briefly acknowledge ("ok, skipping trellis flow this turn"), then inline. **Without seeing one of these phrases you must NOT inline on your own**; do not invent an override the user never said. +No active task. First classify the current turn and ask for task-creation consent before creating any Trellis task. +Simple conversation / small task: ask only whether this turn should create a Trellis task. If the user says no, skip Trellis for this session. +Complex task: ask the user if you can create a Trellis task and enter the planning phase. If the user says no, explain, clarify scope, or suggest a smaller split. [/workflow-state:no_task] ### Phase 1: Plan -- 1.0 Create task `[required · once]` (just `task.py create`; status enters planning) -- 1.1 Requirement exploration `[required · repeatable]` +- 1.0 Create task `[required · once]` (only after task-creation consent) +- 1.1 Requirement exploration `[required · repeatable]` (`prd.md`; complex tasks also need `design.md` + `implement.md`) - 1.2 Research `[optional · repeatable]` -- 1.3 Configure context `[required · once]` — Claude Code, Cursor, OpenCode, Codex, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi -- 1.4 Activate task `[required · once]` (run `task.py start`; status → in_progress) +- 1.3 Configure context `[required · once]` — Claude Code, Cursor, OpenCode, Codex, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Grok, Kimi Code (sub-agent-dispatch platforms only; inline platforms skip) +- 1.4 Activate task `[required · once]` (review gate, then `task.py start`; status → in_progress) - 1.5 Completion criteria <!-- Per-turn breadcrumb: shown throughout Phase 1 (status='planning') --> [workflow-state:planning] -Load the `trellis-brainstorm` skill and iterate on prd.md with the user. -Phase 1.3 (required, once): before `task.py start`, you MUST curate `implement.jsonl` and `check.jsonl` — list the spec / research files sub-agents need so they get the right context injected. You may skip only if the jsonl already has agent-curated entries (the seed `_example` row alone doesn't count). -Then run `task.py start <task-dir>` to flip status to in_progress. +Load `trellis-brainstorm`; stay in planning. +Lightweight: `prd.md` can be enough. Complex: finish `prd.md`, `design.md`, and `implement.md`; ask for review before `task.py start`. +Multi-deliverable scope: consider a parent task plus independently verifiable child tasks; dependencies must be written in child artifacts, not implied by tree position. +Sub-agent mode: curate `implement.jsonl` and `check.jsonl` as spec/research manifests before start. [/workflow-state:planning] <!-- Per-turn breadcrumb: shown throughout Phase 1 when codex.dispatch_mode=inline. Codex-only opt-in alternate to [workflow-state:planning]. The main agent - edits code directly in Phase 2, so Phase 1.3 jsonl curation is skipped — + edits code directly in Phase 2, so jsonl curation is skipped — the inline workflow loads `trellis-before-dev` instead of injecting JSONL into a sub-agent. --> [workflow-state:planning-inline] -Load the `trellis-brainstorm` skill and iterate on prd.md with the user. -Phase 1.3 jsonl curation is **skipped** in inline dispatch mode — the main session loads `trellis-before-dev` directly in Phase 2 and reads spec context itself, so there is no sub-agent to inject jsonl into. -Then run `task.py start <task-dir>` to flip status to in_progress. +Load `trellis-brainstorm`; stay in planning. +Lightweight: `prd.md` can be enough. Complex: finish `prd.md`, `design.md`, and `implement.md`; ask for review before `task.py start`. +Multi-deliverable scope: consider a parent task plus independently verifiable child tasks; dependencies must be written in child artifacts, not implied by tree position. +Inline mode: skip jsonl curation; Phase 2 reads artifacts/specs via `trellis-before-dev`. [/workflow-state:planning-inline] ### Phase 2: Execute @@ -220,18 +241,18 @@ Then run `task.py start <task-dir>` to flip status to in_progress. - 2.3 Rollback `[on demand]` <!-- Per-turn breadcrumb: shown while status='in_progress'. - Scope: all of Phase 2 + Phase 3.1-3.4 (status stays 'in_progress' from + Scope: all of Phase 2 + Phase 3.2-3.4 (status stays 'in_progress' from task.py start until task.py archive; only archive flips it). The body therefore must cover every required step from implementation through commit, including Phase 3.3 spec update and Phase 3.4 commit. --> +Sub-agent dispatch protocol applies to all platforms and all sub-agents, including native Codex `SubagentStart` context injection with child-side pull fallback, class-2 Gemini/Qoder/Copilot/Reasonix/Trae/Grok/Kimi Code, hook-backed ZCode/Snow, and `trellis-research`: every dispatch prompt starts with `Active task: <task path from task.py current>` before role-specific instructions. On Grok Build, use `spawn_subagent` with `subagent_type` set to the Trellis agent name (e.g. `trellis-implement`). On Kimi Code, dispatch the built-in `coder` / `explore` sub-agent with the matching `.kimi-code/skills/trellis-<role>/SKILL.md` instructions. + [workflow-state:in_progress] -**Tools**: `trellis-implement` / `trellis-research` are sub-agent types only (Task/Agent tool, NOT Skill — there is no skill by these names). `trellis-update-spec` is a skill. `trellis-check` exists as both; prefer the Agent form when verifying after code changes. -**Flow**: trellis-implement → trellis-check → trellis-update-spec → commit (Phase 3.4) → `/trellis:finish-work`. -**Main-session default (no override)**: dispatch the `trellis-implement` / `trellis-check` sub-agents — the main agent does NOT edit code by default. Phase 3.4 commit (required, once): after trellis-update-spec, or whenever implementation is verifiably complete, the main agent **drives the commit** — state the commit plan in user-facing text, then run `git commit` — BEFORE suggesting `/trellis:finish-work`. `/finish-work` refuses to run on a dirty working tree (paths outside `.trellis/workspace/` and `.trellis/tasks/`). -**Sub-agent self-exemption**: if you are already running as `trellis-implement`, implement directly from the loaded task context and do NOT spawn another `trellis-implement`; if you are already running as `trellis-check`, review/fix directly and do NOT spawn another `trellis-check`. The default dispatch rule applies to the main session only. -**Sub-agent dispatch protocol (all platforms, all sub-agents)**: When you spawn `trellis-implement` / `trellis-check` / `trellis-research`, your dispatch prompt **MUST** start with one line: `Active task: <task path from \`task.py current\`>`. No exceptions. On class-2 platforms (codex / copilot / gemini / qoder) the sub-agent depends on this line because there is no hook to inject task context. On class-1 platforms (claude / cursor / opencode / kiro / codebuddy / droid) the line is normally redundant — the hook injects context directly — but it serves as a critical fallback when the hook fails (Windows + Claude Code PreToolUse silent skip, `--continue` resume, fork distribution, hooks disabled, etc.). For `trellis-research`, the line tells the sub-agent which `{task_dir}/research/` to write into. -**Inline override** (per-turn only, escape hatch for sub-agent dispatch): the user's CURRENT message MUST explicitly contain one of: "do it inline" / "no sub-agent" / "你直接改" / "别派 sub-agent" / "main session 写就行" / "不用 sub-agent". **Without seeing one of these phrases you must NOT inline on your own**; do not invent an override the user never said. +Tools: `trellis-implement` / `trellis-research` are sub-agent types only (Task/Agent tool, NOT Skill; there is no skill by these names). `trellis-update-spec` is a skill. `trellis-check` exists as both; prefer the Agent form when verifying after code changes. +Flow: `trellis-implement` -> `trellis-check` -> `trellis-update-spec` -> commit (Phase 3.4) -> `/trellis:finish-work`. +Main-session default: dispatch implement/check sub-agents. Sub-agent self-exemption: if already running as `trellis-implement`, do NOT spawn another `trellis-implement` or `trellis-check`; if already running as `trellis-check`, do NOT spawn another `trellis-check` or `trellis-implement`. Dispatch is main session only. +Dispatch prompt starts with `Active task: <task path from task.py current>`. Read context: jsonl entries -> `prd.md` -> `design.md if present` -> `implement.md if present`. [/workflow-state:in_progress] <!-- Per-turn breadcrumb: shown while status='in_progress' when @@ -240,18 +261,19 @@ Then run `task.py start <task-dir>` to flip status to in_progress. instead of dispatching sub-agents. --> [workflow-state:in_progress-inline] -**Flow** (inline mode): main session loads `trellis-before-dev` → main session edits code → main session loads `trellis-check` → run lint / type-check / tests → fix → `trellis-update-spec` → commit (Phase 3.4) → `/trellis:finish-work`. -**Main-session default (inline dispatch_mode)**: the main agent edits code directly. Do NOT dispatch `trellis-implement` / `trellis-check` sub-agents. Load the `trellis-before-dev` skill before writing code; load the `trellis-check` skill before reporting completion. -Phase 3.4 commit (required, once): after `trellis-update-spec`, or whenever implementation is verifiably complete, the main agent **drives the commit** — state the commit plan in user-facing text, then run `git commit` — BEFORE suggesting `/trellis:finish-work`. `/finish-work` refuses to run on a dirty working tree (paths outside `.trellis/workspace/` and `.trellis/tasks/`). +Flow: `trellis-before-dev` -> edit -> `trellis-check` -> validation -> `trellis-update-spec` -> commit (Phase 3.4) -> `/trellis:finish-work`. +Do not dispatch implement/check sub-agents in inline mode. +Read context: `prd.md` -> `design.md if present` -> `implement.md if present`, plus relevant spec/research loaded by skills. [/workflow-state:in_progress-inline] ### Phase 3: Finish -- 3.1 Quality verification `[required · repeatable]` - 3.2 Debug retrospective `[on demand]` - 3.3 Spec update `[required · once]` - 3.4 Commit changes `[required · once]` - 3.5 Wrap-up reminder +> Note: step 3.1 was folded into 2.2 (last-iteration full-scope check) and 3.4 (commit preamble). Numbering kept stable to avoid breaking external references. + <!-- Per-turn breadcrumb: shown while status='completed'. Currently DEAD in normal flow: cmd_archive writes status='completed' in the same call that moves the task dir to archive/, so the active-task @@ -261,9 +283,7 @@ Phase 3.4 commit (required, once): after `trellis-update-spec`, or whenever impl channel as the live blocks. --> [workflow-state:completed] -Code committed via Phase 3.4; run `/trellis:finish-work` to wrap up (archive the task + record session). -If you reach this state with uncommitted code, return to Phase 3.4 first — `/finish-work` refuses to run on a dirty working tree. -`task.py archive` deletes any runtime session files that still point at the archived task. +Code committed. Run `/trellis:finish-work`; if dirty, return to Phase 3.4 first. [/workflow-state:completed] ### Rules @@ -272,60 +292,33 @@ If you reach this state with uncommitted code, return to Phase 3.4 first — `/f 2. Run steps in order inside each Phase; `[required]` steps can't be skipped 3. Phases can roll back (e.g., Execute reveals a prd defect → return to Plan to fix, then re-enter Execute) 4. Steps tagged `[once]` are skipped if the output already exists; don't re-run +5. Artifact presence informs the next step; missing `design.md` / `implement.md` is valid for lightweight tasks and incomplete planning for complex tasks. -### Skill Routing +### Active Task Routing -When a user request matches one of these intents, load the corresponding skill (or dispatch the corresponding sub-agent) first — do not skip skills. +When a user request matches one of these intents inside an active task, route first, then load the detailed phase step if needed. -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code] -| User intent | Route | -|---|---| -| Wants a new feature / requirement unclear | `trellis-brainstorm` | -| About to write code / start implementing | Dispatch the `trellis-implement` sub-agent per Phase 2.1 | -| Finished writing / want to verify | Dispatch the `trellis-check` sub-agent per Phase 2.2 | -| Stuck / fixed same bug several times | `trellis-break-loop` | -| Spec needs update | `trellis-update-spec` | +- Planning or unclear requirements -> `trellis-brainstorm`. +- `in_progress` implementation/check -> dispatch `trellis-implement` / `trellis-check`. +- Repeated debugging -> `trellis-break-loop`; spec updates -> `trellis-update-spec`. -**Why `trellis-before-dev` is NOT in this table:** you are not the one writing code — the `trellis-implement` sub-agent is. Sub-agent platforms get spec context via `implement.jsonl` injection / prelude, not via the main thread loading `trellis-before-dev`. +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code] -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[codex-inline, Kilo, Antigravity, Devin] -[codex-inline, Kilo, Antigravity, Windsurf] +- Planning or unclear requirements -> `trellis-brainstorm`. +- Before editing -> `trellis-before-dev`; after editing -> `trellis-check`. +- Repeated debugging -> `trellis-break-loop`; spec updates -> `trellis-update-spec`. -| User intent | Skill | -|---|---| -| Wants a new feature / requirement unclear | `trellis-brainstorm` | -| About to write code / start implementing | `trellis-before-dev` (then implement directly in the main session) | -| Finished writing / want to verify | `trellis-check` | -| Stuck / fixed same bug several times | `trellis-break-loop` | -| Spec needs update | `trellis-update-spec` | +[/codex-inline, Kilo, Antigravity, Devin] -[/codex-inline, Kilo, Antigravity, Windsurf] +### Guardrails -### DO NOT skip skills - -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -| What you're thinking | Why it's wrong | -|---|---| -| "This is simple, I'll just code it in the main thread" | Dispatching `trellis-implement` is the cheap path; skipping it tempts you to write code in the main thread and lose spec context — sub-agents get `implement.jsonl` injected, you don't | -| "I already thought it through in plan mode" | Plan-mode output lives in memory — sub-agents can't see it; must be persisted to prd.md | -| "I already know the spec" | The spec may have been updated since you last read it; the sub-agent gets the fresh copy, you may not | -| "Code first, check later" | `trellis-check` surfaces issues you won't notice yourself; earlier is cheaper | - -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -[codex-inline, Kilo, Antigravity, Windsurf] - -| What you're thinking | Why it's wrong | -|---|---| -| "This is simple, just code it" | Simple tasks often grow complex; `trellis-before-dev` takes under a minute and loads the spec context you'll need | -| "I already thought it through in plan mode" | Plan-mode output lives in memory — must be persisted to prd.md before code | -| "I already know the spec" | The spec may have been updated since you last read it; read again | -| "Code first, check later" | `trellis-check` surfaces issues you won't notice yourself; earlier is cheaper | - -[/codex-inline, Kilo, Antigravity, Windsurf] +- Task creation approval is not implementation approval; implementation waits for `task.py start` after artifact review. +- PRD-only is valid for lightweight tasks; complex tasks need `design.md` + `implement.md`. +- Planning must be persisted to task artifacts; checks must run before reporting completion. ### Loading Step Detail @@ -340,11 +333,11 @@ python3 ./.trellis/scripts/get_context.py --mode phase --step <step> ## Phase 1: Plan -Goal: figure out what to build, produce a clear requirements doc and the context needed to implement it. +Goal: classify the request, get task-creation consent when a task is needed, and produce the planning artifacts required before implementation. #### 1.0 Create task `[required · once]` -Create the task directory (status enters `planning`, the session active-task pointer auto-targets the new task when session identity is available): +Create the task directory only after task-creation consent. The command sets status to `planning`, writes `task.json`, creates a default `prd.md`, and auto-targets the new task when session identity is available: ```bash python3 ./.trellis/scripts/task.py create "<task title>" --slug <name> @@ -352,9 +345,11 @@ python3 ./.trellis/scripts/task.py create "<task title>" --slug <name> `--slug` is the human-readable name only. Do **not** include the `MM-DD-` date prefix; `task.py create` adds that prefix automatically. -After this command succeeds, the per-turn breadcrumb auto-switches to `[workflow-state:planning]`, telling the AI to enter the brainstorm + jsonl curation phase. +For task trees, create the parent task first and then create each child with `--parent <parent-dir>`. Do not start the parent just because children exist; start the child that owns the next independently verifiable deliverable. -⚠️ **Run only `create` here — do not also run `start`**. `start` flips status to `in_progress`, which switches the breadcrumb to the implementation phase before brainstorm + jsonl are done — the AI will silently skip them. Save `start` for step 1.4, after jsonl curation is complete. +After this command succeeds, the per-turn breadcrumb auto-switches to `[workflow-state:planning]`, telling the AI to stay in planning. + +Run only `create` here — do not also run `start`. `start` flips status to `in_progress`, which switches the breadcrumb to the implementation phase before planning artifacts are reviewed. Save `start` for step 1.4. Skip when `python3 ./.trellis/scripts/task.py current --source` already points to a task. @@ -367,14 +362,24 @@ The brainstorm skill will guide you to: - Prefer researching over asking the user - Prefer offering options over open-ended questions - Update `prd.md` immediately after each user answer +- Split large scopes into a parent task plus child tasks when the deliverables can be verified independently +- Keep `prd.md` focused on requirements and acceptance criteria +- For complex tasks, produce `design.md` and `implement.md` before implementation starts -Return to this step whenever requirements change and revise `prd.md`. +When considering a parent/child split: +- Use a parent task when one request contains several independently verifiable deliverables. +- Parent tasks own source requirements, child-task mapping, cross-child acceptance criteria, and final integration review. +- Child tasks own actual deliverables that can be planned, implemented, checked, and archived independently. +- Parent/child structure is not a dependency system. If child B depends on child A, write that ordering in child B's `prd.md` / `implement.md`. +- Start the child task that owns the next deliverable. Do not start the parent unless the parent itself has direct implementation work. + +Return to this step whenever requirements change and revise the relevant artifact. #### 1.2 Research `[optional · repeatable]` Research can happen at any time during requirement exploration. It isn't limited to local code — you can use any available tool (MCP servers, skills, web search, etc.) to look up external information, including third-party library docs, industry practices, API references, etc. -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code] Spawn the research sub-agent: @@ -382,13 +387,13 @@ Spawn the research sub-agent: - **Task description**: Research <specific question> - **Key requirement**: Research output MUST be persisted to `{TASK_DIR}/research/` -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code] -[codex-inline, Kilo, Antigravity, Windsurf] +[codex-inline, Kilo, Antigravity, Devin] -Do the research in the main session directly and write findings into `{TASK_DIR}/research/`. (For `codex-inline` this avoids the `fork_turns="none"` isolation that prevents `trellis-research` sub-agents from resolving the active task path.) +Do the research in the main session directly and write findings into `{TASK_DIR}/research/`. `codex-inline` is the explicit mode that keeps work in the main session. -[/codex-inline, Kilo, Antigravity, Windsurf] +[/codex-inline, Kilo, Antigravity, Devin] **Research artifact conventions**: - One file per research topic (e.g. `research/auth-library-comparison.md`) @@ -401,9 +406,9 @@ Brainstorm and research can interleave freely — pause to research a technical #### 1.3 Configure context `[required · once]` -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code] -Curate `implement.jsonl` and `check.jsonl` so the Phase 2 sub-agents get the right spec context. These files were seeded on `task create` with a single self-describing `_example` line; your job here is to fill in real entries. +Curate `implement.jsonl` and `check.jsonl` so the Phase 2 sub-agents get the right spec/research context. These files were seeded on `task create` with a single self-describing `_example` line; your job here is to fill in real entries. **Location**: `{TASK_DIR}/implement.jsonl` and `{TASK_DIR}/check.jsonl` (already exist). @@ -421,6 +426,8 @@ Curate `implement.jsonl` and `check.jsonl` so the Phase 2 sub-agents get the rig - `implement.jsonl` → specs + research the implement sub-agent needs to write code correctly - `check.jsonl` → specs for the check sub-agent (quality guidelines, check conventions, same research if needed) +These manifests do not replace `implement.md`. `implement.md` is the human-readable execution plan for a complex task; jsonl files only list context files to inject or load. + **How to discover relevant specs**: ```bash @@ -440,24 +447,28 @@ python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" check "<path>" "<reas Delete the seed `_example` line once real entries exist (optional — it's skipped automatically by consumers). -Skip when: `implement.jsonl` has agent-curated entries (the seed row alone doesn't count). +Ready gate: both `implement.jsonl` and `check.jsonl` must contain at least one real `{"file": "...", "reason": "..."}` entry before `task.py start`. The seed `_example` row alone is not ready. -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +Skip this step only when both files already have real curated entries. -[codex-inline, Kilo, Antigravity, Windsurf] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code] + +[codex-inline, Kilo, Antigravity, Devin] Skip this step. Context is loaded directly by the `trellis-before-dev` skill in Phase 2. -[/codex-inline, Kilo, Antigravity, Windsurf] +[/codex-inline, Kilo, Antigravity, Devin] #### 1.4 Activate task `[required · once]` -Once prd.md is complete and 1.3 jsonl curation is done, flip the task status to `in_progress`: +After artifact review, flip the task status to `in_progress`: ```bash python3 ./.trellis/scripts/task.py start <task-dir> ``` +For lightweight tasks, `prd.md` can be enough. For complex tasks, `prd.md`, `design.md`, and `implement.md` must exist and be reviewed before start. On sub-agent-dispatch platforms, `implement.jsonl` and `check.jsonl` must both have real curated entries before start. Runtime consumers tolerate missing or seed-only manifests for compatibility, but that tolerance is not a planning-ready state. + After this command succeeds, the breadcrumb auto-switches to `[workflow-state:in_progress]`, and the rest of Phase 2 / 3 follows. If `task.py start` errors with a session-identity message (no context key from hook input, `TRELLIS_CONTEXT_ID`, or platform-native session env), follow the hint in the error to set up session identity, then retry. @@ -467,95 +478,98 @@ If `task.py start` errors with a session-identity message (no context key from h | Condition | Required | |------|:---:| | `prd.md` exists | ✅ | -| User confirms requirements | ✅ | +| User confirms task should enter implementation | ✅ | | `task.py start` has been run (status = in_progress) | ✅ | | `research/` has artifacts (complex tasks) | recommended | -| `info.md` technical design (complex tasks) | optional | +| `design.md` exists (complex tasks) | ✅ | +| `implement.md` exists (complex tasks) | ✅ | -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code] -| `implement.jsonl` has agent-curated entries (not just the seed row) | ✅ | +| `implement.jsonl` and `check.jsonl` each contain at least one real curated entry (seed row does not count) | ✅ | -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code] --- ## Phase 2: Execute -Goal: turn the prd into code that passes quality checks. +Goal: turn reviewed planning artifacts into code that passes quality checks. #### 2.1 Implement `[required · repeatable]` -[Claude Code, Cursor, OpenCode, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, CodeBuddy, Droid, Pi, ZCode, Snow, Oh My Pi] Spawn the implement sub-agent: - **Agent type**: `trellis-implement` -- **Task description**: Implement the requirements per prd.md, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check -- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-implement` sub-agent and must implement directly, not spawn another `trellis-implement` / `trellis-check`. +- **Task description**: Implement the reviewed task artifacts, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check +- **Dispatch prompt guard**: The prompt MUST start with `Active task: <task path>`, then tell the spawned agent it is already the `trellis-implement` sub-agent and must implement directly, not spawn another `trellis-implement` / `trellis-check`. The platform hook/plugin auto-handles: -- Reads `implement.jsonl` and injects the referenced spec files into the agent prompt -- Injects prd.md content +- Reads `implement.jsonl` and injects referenced spec/research files into the agent prompt +- Injects `prd.md`, `design.md` if present, and `implement.md` if present +- For Codex, `SubagentStart` supplies native context injection; the agent profile keeps child-side loading as the fallback -[/Claude Code, Cursor, OpenCode, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, CodeBuddy, Droid, Pi, ZCode, Snow, Oh My Pi] -[codex-sub-agent] +[Gemini, Qoder, Copilot, Reasonix, Trae, Grok, Kimi Code] Spawn the implement sub-agent: - **Agent type**: `trellis-implement` -- **Task description**: Implement the requirements per prd.md, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check +- **Task description**: Implement the reviewed task artifacts, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check - **Dispatch prompt guard**: The prompt MUST start with `Active task: <task path>`, then explicitly say the spawned agent is already `trellis-implement` and must implement directly without spawning another `trellis-implement` / `trellis-check`. -The Codex sub-agent definition auto-handles the context load requirement: -- Resolves the active task with `task.py current --source`, then reads `prd.md` and `info.md` if present -- Reads `implement.jsonl` and requires the agent to load each referenced spec file before coding +The pull-based sub-agent definition auto-handles the context load requirement: +- Resolves the active task with `task.py current --source`, then reads `prd.md`, `design.md` if present, and `implement.md` if present +- Reads `implement.jsonl` and requires the agent to load each referenced spec/research file before coding -[/codex-sub-agent] +[/Gemini, Qoder, Copilot, Reasonix, Trae, Grok, Kimi Code] [Kiro] Spawn the implement sub-agent: - **Agent type**: `trellis-implement` -- **Task description**: Implement the requirements per prd.md, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check +- **Task description**: Implement the reviewed task artifacts, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check - **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-implement` sub-agent and must implement directly, not spawn another `trellis-implement` / `trellis-check`. The platform prelude auto-handles the context load requirement: -- Reads `implement.jsonl` and injects the referenced spec files into the agent prompt -- Injects prd.md content +- Reads `implement.jsonl` and injects referenced spec/research files into the agent prompt +- Injects `prd.md`, `design.md` if present, and `implement.md` if present [/Kiro] -[codex-inline, Kilo, Antigravity, Windsurf] +[codex-inline, Kilo, Antigravity, Devin] 1. Load the `trellis-before-dev` skill to read project guidelines -2. Read `{TASK_DIR}/prd.md` for requirements +2. Read `{TASK_DIR}/prd.md`, then `design.md` if present, then `implement.md` if present 3. Consult materials under `{TASK_DIR}/research/` -4. Implement the code per requirements +4. Implement the code per reviewed artifacts 5. Run project lint and type-check -[/codex-inline, Kilo, Antigravity, Windsurf] +[/codex-inline, Kilo, Antigravity, Devin] #### 2.2 Quality check `[required · repeatable]` -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code] Spawn the check sub-agent: - **Agent type**: `trellis-check` -- **Task description**: Review all code changes against spec and prd; fix any findings directly; ensure lint and type-check pass -- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-check` sub-agent and must review/fix directly, not spawn another `trellis-check` / `trellis-implement`. +- **Task description**: Review all code changes against specs and task artifacts; fix any findings directly; ensure lint and type-check pass +- **Dispatch prompt guard**: The prompt MUST start with `Active task: <task path>`, then tell the spawned agent it is already the `trellis-check` sub-agent and must review/fix directly, not spawn another `trellis-check` / `trellis-implement`. The check agent's job: - Review code changes against specs +- Review code changes against `prd.md`, `design.md` if present, and `implement.md` if present - Auto-fix issues it finds - Run lint and typecheck to verify -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Snow, Reasonix, Trae, Grok, Kimi Code] -[codex-inline, Kilo, Antigravity, Windsurf] +[codex-inline, Kilo, Antigravity, Devin] Load the `trellis-check` skill and verify the code per its guidance: - Spec compliance @@ -564,7 +578,9 @@ Load the `trellis-check` skill and verify the code per its guidance: If issues are found → fix → re-check, until green. -[/codex-inline, Kilo, Antigravity, Windsurf] +[/codex-inline, Kilo, Antigravity, Devin] + +**Final pass (before Phase 3.4 commit)**: the last 2.2 of a task must run full-scope, not just on the latest implement chunk. List all affected packages with `python3 ./.trellis/scripts/get_context.py --mode packages`, then load each package's spec index Quality Check section. This catches cross-layer / multi-package issues a mid-iteration local 2.2 cannot. #### 2.3 Rollback `[on demand]` @@ -578,15 +594,6 @@ If issues are found → fix → re-check, until green. Goal: ensure code quality, capture lessons, record the work. -#### 3.1 Quality verification `[required · repeatable]` - -Load the `trellis-check` skill and do a final verification: -- Spec compliance -- lint / type-check / tests -- Cross-layer consistency (when changes span layers) - -If issues are found → fix → re-check, until green. - #### 3.2 Debug retrospective `[on demand]` If this task involved repeated debugging (the same issue was fixed multiple times), load the `trellis-break-loop` skill to: @@ -607,6 +614,8 @@ Update the docs under `.trellis/spec/` accordingly. Even if the conclusion is "n #### 3.4 Commit changes `[required · once]` +**Spec-sync preamble**: before drafting commits, ask: did this task fix a bug or surface non-obvious knowledge that should land in `.trellis/spec/` so future-you (or future-AI) doesn't repeat the mistake? If yes, return to Phase 3.3 first — spec writes belong in the same task's commit batch, not as a forgotten follow-up. + The AI drives a batched commit of this task's code changes so `/finish-work` can run cleanly afterwards. Goal: produce work commits FIRST, then bookkeeping (archive + journal) commits land after — never interleaved. **Step-by-step**: @@ -667,15 +676,20 @@ This section is for developers who want to modify the Trellis workflow itself. A ### Changing what a step means -Edit the corresponding step's walkthrough body in the Phase 1 / 2 / 3 sections above. **Critical constraint**: if you change a step's `[required · once]` marker or add a new `[required · once]` step, you MUST also add a matching enforcement line to that phase's `[workflow-state:STATUS]` tag block — otherwise the per-turn breadcrumb omits the reinforcement, and the AI silently skips the step. The regression tests assert this. +Edit the corresponding step's walkthrough body in the Phase 1 / 2 / 3 sections above. Critical invariants: +- No active task must triage first and ask for task-creation consent before creating a Trellis task. +- Planning must distinguish lightweight PRD-only tasks from complex tasks that require `prd.md`, `design.md`, and `implement.md` before start. +- Every required execution path must keep the Phase 3.4 commit reminder reachable before `/trellis:finish-work`. -All 4 tag blocks live in the `## Phase Index` section above, immediately after each phase summary: +All tag blocks live in the `## Phase Index` section above, immediately after each phase summary: | Scope | Corresponding tag | |---|---| | No active task (before Phase 1) | `[workflow-state:no_task]` (after the Phase Index ASCII art) | | All of Phase 1 (task created → ready for implementation) | `[workflow-state:planning]` (after Phase 1 summary) | -| Phase 2 + Phase 3.1–3.4 (implementation + check + wrap-up) | `[workflow-state:in_progress]` (after Phase 2 summary) | +| Codex inline Phase 1 | `[workflow-state:planning-inline]` | +| Phase 2 + Phase 3.2–3.4 (implementation + check + wrap-up) | `[workflow-state:in_progress]` (after Phase 2 summary) | +| Codex inline Phase 2 + Phase 3.2–3.4 | `[workflow-state:in_progress-inline]` | | After Phase 3.5 (archived) | `[workflow-state:completed]` (after Phase 3 summary; **currently DEAD**) | ### Changing the per-turn prompt text