diff --git a/.agents/skills/trellis-before-dev/SKILL.md b/.agents/skills/trellis-before-dev/SKILL.md new file mode 100644 index 0000000..5a4b852 --- /dev/null +++ b/.agents/skills/trellis-before-dev/SKILL.md @@ -0,0 +1,40 @@ +--- +name: trellis-before-dev +description: "Discovers and injects project-specific coding guidelines from .trellis/spec/ before implementation begins. Reads spec indexes, pre-development checklists, and shared thinking guides for the target package. Use when starting a new coding task, before writing any code, switching to a different package, or needing to refresh project conventions and standards." +--- + +Read the relevant development guidelines before starting your task. + +Execute these steps: + +1. **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 + ``` + +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 + +4. **Read the spec index** for each relevant module: + ```bash + cat .trellis/spec///index.md + ``` + Follow the **"Pre-Development Checklist"** section in the index. + +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. + +6. **Always read shared guides**: + ```bash + cat .trellis/spec/guides/index.md + ``` + +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 new file mode 100644 index 0000000..806ee31 --- /dev/null +++ b/.agents/skills/trellis-brainstorm/SKILL.md @@ -0,0 +1,173 @@ +--- +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." +--- + +# Trellis Brainstorm + +## Non-Negotiable Interview Contract + +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. + +Ask the questions one at a time. + +## 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. + +Do not ask the user to confirm facts that the repository can answer. Ask only for product intent, preference, scope, risk tolerance, or decisions that remain ambiguous after inspection. + +--- + +Use this skill during Phase 1 planning to turn the user's request into clear requirements and planning artifacts. + +## Preconditions + +Use this skill only after task-creation consent has been given and the user is ready to enter Trellis planning. + +If no task exists yet, create one: + +```bash +TASK_DIR=$(python3 ./.trellis/scripts/task.py create "" --slug ) +``` + +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. + +`task.py create` creates the default `prd.md`. Update that file with the current understanding before asking follow-up questions. + +## Planning Flow + +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. Ask the single highest-value remaining question. +5. Include your recommended answer with the question. +6. After each user answer, update `prd.md` before continuing. +7. For complex tasks, create or update `design.md` and `implement.md` before implementation starts. +8. Before final review or `task.py start`, run the PRD convergence pass below. + +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. + +## Question Rules + +Ask only one question per message. + +Each question must include: + +- the decision needed +- why the answer matters +- your recommended answer +- the trade-off if the user chooses differently + +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. + +## 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. + +### Step 1: Restate the Problem + +Strip away implementation details to one sentence. + +> 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 + +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 + +For each component of the current plan: + +- **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. + +### Step 4: Build Up from Truths + +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?" + +### Step 5: Validate + +- Does the solution solve the original problem? +- What assumptions need verification? +- What's the simplest experiment to test this? + +## Artifact Rules + +`prd.md` records requirements and acceptance: + +- goal and user value +- confirmed facts +- requirements +- acceptance criteria +- out of scope +- open questions that still block planning + +`design.md` records technical design for complex tasks: + +- architecture and boundaries +- data flow and contracts +- compatibility and migration notes +- important trade-offs +- operational or rollback considerations + +`implement.md` records execution planning for complex tasks: + +- ordered implementation checklist +- validation commands +- risky files or rollback points +- follow-up checks before `task.py start` + +Lightweight tasks may have only `prd.md`. Complex tasks must have `prd.md`, `design.md`, and `implement.md` before `task.py start`. + +`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`. + +## PRD Convergence Pass + +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. + +The pass must be lossless: + +- 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. +- Keep only genuinely blocking open questions. + +After the pass, read `prd.md` top to bottom and verify that no fact is repeated across sections unless the repetition adds new information. + +## Quality Bar + +Before declaring planning ready: + +- `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. +- Remaining open questions are genuinely about user intent or scope. +- 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 user has reviewed the final planning artifacts or explicitly approved proceeding. + +Do not start implementation until the user approves or asks for implementation. diff --git a/.agents/skills/trellis-break-loop/SKILL.md b/.agents/skills/trellis-break-loop/SKILL.md new file mode 100644 index 0000000..1c8b397 --- /dev/null +++ b/.agents/skills/trellis-break-loop/SKILL.md @@ -0,0 +1,188 @@ +--- +name: trellis-break-loop +description: "Deep bug analysis to break the fix-forget-repeat cycle. Analyzes root cause category, why fixes failed, prevention mechanisms, and captures knowledge into specs. Use after fixing a bug to prevent the same class of bugs." +--- + +# Break the Loop - Deep Bug Analysis + +When debug is complete, use this for deep analysis to break the "fix bug -> forget -> repeat" cycle. + +--- + +## Analysis Framework + +Analyze the bug you just fixed from these 5 dimensions: + +### 1. Root Cause Category + +Which category does this bug belong to? + +| Category | Characteristics | Example | +|----------|-----------------|---------| +| **A. Missing Spec** | No documentation on how to do it | New feature without checklist | +| **B. Cross-Layer Contract** | Interface between layers unclear | API returns different format than expected | +| **C. Change Propagation Failure** | Changed one place, missed others | Changed function signature, missed call sites | +| **D. Test Coverage Gap** | Unit test passes, integration fails | Works alone, breaks when combined | +| **E. Implicit Assumption** | Code relies on undocumented assumption | Timestamp seconds vs milliseconds | + +### 2. Why Fixes Failed (if applicable) + +If you tried multiple fixes before succeeding, analyze each failure: + +- **Surface Fix**: Fixed symptom, not root cause +- **Incomplete Scope**: Found root cause, didn't cover all cases +- **Tool Limitation**: grep missed it, type check wasn't strict +- **Mental Model**: Kept looking in same layer, didn't think cross-layer + +### 3. Prevention Mechanisms + +What mechanisms would prevent this from happening again? + +| Type | Description | Example | +|------|-------------|---------| +| **Documentation** | Write it down so people know | Update thinking guide | +| **Architecture** | Make the error impossible structurally | Type-safe wrappers | +| **Compile-time** | Strict type checking, no escape hatches | Signature change causes compile error | +| **Runtime** | Monitoring, alerts, scans | Detect orphan entities | +| **Test Coverage** | E2E tests, integration tests | Verify full flow | +| **Code Review** | Checklist, PR template | "Did you check X?" | + +### 4. Systematic Expansion + +What broader problems does this bug reveal? + +- **Similar Issues**: Where else might this problem exist? +- **Design Flaw**: Is there a fundamental architecture issue? +- **Process Flaw**: Is there a development process improvement? +- **Knowledge Gap**: Is the team missing some understanding? + +### 5. Knowledge Capture + +Solidify insights into the system: + +- [ ] Update `.trellis/spec/guides/` thinking guides +- [ ] Update relevant `.trellis/spec/` docs +- [ ] Create issue record (if applicable) +- [ ] Create feature ticket for root fix +- [ ] Update check guidelines if needed + +--- + +## Output Format + +Please output analysis in this format: + +```markdown +## Bug Analysis: [Short Description] + +### 1. Root Cause Category +- **Category**: [A/B/C/D/E] - [Category Name] +- **Specific Cause**: [Detailed description] + +### 2. Why Fixes Failed (if applicable) +1. [First attempt]: [Why it failed] +2. [Second attempt]: [Why it failed] +... + +### 3. Prevention Mechanisms +| Priority | Mechanism | Specific Action | Status | +|----------|-----------|-----------------|--------| +| P0 | ... | ... | TODO/DONE | + +### 4. Systematic Expansion +- **Similar Issues**: [List places with similar problems] +- **Design Improvement**: [Architecture-level suggestions] +- **Process Improvement**: [Development process suggestions] + +### 5. Knowledge Capture +- [ ] [Documents to update / tickets to create] +``` + +--- + +## Core Philosophy + +> **The value of debugging is not in fixing the bug, but in making this class of bugs never happen again.** + +Three levels of insight: +1. **Tactical**: How to fix THIS bug +2. **Strategic**: How to prevent THIS CLASS of bugs +3. **Philosophical**: How to expand thinking patterns + +30 minutes of analysis saves 30 hours of future debugging. + +## 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 + +**IMPORTANT**: After completing the analysis above, you MUST immediately: + +1. **Update spec/guides** - Don't just list TODOs, actually update the relevant files: + - If it's a cross-platform issue → update `cross-platform-thinking-guide.md` + - If it's a cross-layer issue → update `cross-layer-thinking-guide.md` + - If it's a code reuse issue → update `code-reuse-thinking-guide.md` + - If it's domain-specific → update `backend/*.md` or `frontend/*.md` + +2. **Sync templates** - After updating `.trellis/spec/`, sync to `src/templates/markdown/spec/` + +3. **Commit the spec updates** - This is the primary output, not just the analysis text + +> **The analysis is worthless if it stays in chat. The value is in the updated specs.** diff --git a/.agents/skills/trellis-channel/SKILL.md b/.agents/skills/trellis-channel/SKILL.md new file mode 100644 index 0000000..511ee02 --- /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 0000000..75def26 --- /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 0000000..06b7f36 --- /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 0000000..3ed40d6 --- /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 0000000..bcec98f --- /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 0000000..3319764 --- /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 new file mode 100644 index 0000000..c695abd --- /dev/null +++ b/.agents/skills/trellis-check/SKILL.md @@ -0,0 +1,98 @@ +--- +name: trellis-check +description: "Comprehensive quality verification: spec compliance, lint, type-check, tests, cross-layer data flow, code reuse, and consistency checks. Use when code is written and needs quality verification, before committing changes, or to catch context drift during long sessions." +--- + +# Code Quality Check + +Comprehensive quality verification for recently written code. Combines spec compliance, cross-layer safety, and pre-commit checks. + +--- + +## Step 1: Identify What Changed + +```bash +git diff --name-only HEAD +git status +``` + +## Step 2: Read 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 +``` + +For each changed package/layer, read the spec index and follow its **Quality Check** section: + +```bash +cat .trellis/spec/<package>/<layer>/index.md +``` + +Read the specific guideline files referenced — the index is a pointer, not the goal. + +## Step 3: Run Project Checks + +Run the project's lint, type-check, and test commands. Fix any failures before proceeding. + +## Step 4: Review Against Checklist + +### Code Quality + +- [ ] Linter passes? +- [ ] Type checker passes (if applicable)? +- [ ] Tests pass? +- [ ] No debug logging left in? +- [ ] No suppressed warnings or type-safety bypasses? + +### Test Coverage + +- [ ] New function → unit test added? +- [ ] Bug fix → regression test added? +- [ ] Changed behavior → existing tests updated? + +### Spec Sync + +- [ ] Does `.trellis/spec/` need updates? (new patterns, conventions, lessons learned) + +> "If I fixed a bug or discovered something non-obvious, should I document it so future me won't hit the same issue?" → If YES, update the relevant spec doc. + +## Step 5: Cross-Layer Dimensions (if applicable) + +Skip this step if your change is confined to a single layer. + +### A. Data Flow (changes touch 3+ layers) + +- [ ] Read flow traces correctly: Storage → Service → API → UI +- [ ] Write flow traces correctly: UI → API → Service → Storage +- [ ] Types/schemas correctly passed between layers? +- [ ] Errors properly propagated to caller? + +### B. Code Reuse (modifying constants, creating utilities) + +- [ ] Searched for existing similar code before creating new? + ```bash + grep -r "pattern" src/ + ``` +- [ ] If 2+ places define same value → extracted to shared constant? +- [ ] After batch modification, all occurrences updated? + +### C. Import/Dependency (creating new files) + +- [ ] Correct import paths (relative vs absolute)? +- [ ] No circular dependencies? + +### D. Same-Layer Consistency + +- [ ] Other places using the same concept are consistent? + +--- + +## Step 6: Report and Fix + +Report violations found and fix them directly. Re-run project checks after fixes. diff --git a/.agents/skills/trellis-continue/SKILL.md b/.agents/skills/trellis-continue/SKILL.md new file mode 100644 index 0000000..4aceae7 --- /dev/null +++ b/.agents/skills/trellis-continue/SKILL.md @@ -0,0 +1,61 @@ +--- +name: trellis-continue +description: "Resume work on the current task. Loads the workflow Phase Index, figures out which phase/step to pick up at, then pulls the step-level detail via get_context.py --mode phase. Use when coming back to an in-progress task and you need to know what to do next." +--- + +# Continue Current Task + +Resume work on the current task — pick up at the right phase/step in `.trellis/workflow.md`. + +--- + +## Step 1: Load Current Context + +```bash +python3 ./.trellis/scripts/get_context.py +``` + +Confirms: current task, git state, recent commits. + +## Step 2: Load the Phase Index + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase +``` + +Shows the Phase Index (Plan / Execute / Finish) with routing + skill mapping. + +## Step 3: Decide Where You Are + +`get_context.py` shows the active task's `status` field. Route by `status` + artifact presence. 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` 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.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 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 + +Once you know which step to resume at: + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase --step <X.X> --platform codex +``` + +Follow the loaded instructions. After each `[required]` step completes, move to the next. + +--- + +## Reference + +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-finish-work/SKILL.md b/.agents/skills/trellis-finish-work/SKILL.md new file mode 100644 index 0000000..5caebb5 --- /dev/null +++ b/.agents/skills/trellis-finish-work/SKILL.md @@ -0,0 +1,71 @@ +--- +name: trellis-finish-work +description: "Wrap up the current session: verify quality gate passed, remind user to commit, archive completed tasks, and record session progress to the developer journal. Use when done coding and ready to end the session." +--- + +# Finish Work + +Wrap up the current session: archive the active task (and any other completed-but-unarchived tasks the user wants to clean up) and record the session journal. Code commits are NOT done here — those happen in workflow Phase 3.4 before you invoke this command. + +## Step 1: Survey current state + +```bash +python3 ./.trellis/scripts/get_context.py --mode record +``` + +This prints: + +- **My active tasks** — review whether any besides the current one are actually done (code merged, AC met) and should be archived this round. +- **Git status** — quick visual on what's dirty. +- **Recent commits** — you'll need their hashes in Step 4 for `--commit`. + +If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless. + +## Step 2: Sanity check — classify dirty paths + +Run: + +```bash +git status --porcelain +``` + +Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this skill's own work. + +For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics: + +- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task +- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task +- Paths in unrelated areas you have no recollection of touching this session → other parallel work + +Then route: + +- **Any remaining path looks like current-task work** — bail out with: + > "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running ``finish-work` (Trellis command)`." + + Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there. +- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3: + > "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`." +- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer. + +## Step 3: Archive task(s) + +```bash +python3 ./.trellis/scripts/task.py archive <task-name> +``` + +At minimum: the current active task (if any). Plus any extra tasks the user confirmed in Step 1. Each archive produces a `chore(task): archive ...` commit via the script's auto-commit. + +If there is no active task and the user did not confirm any cleanup archives, skip this step. + +## Step 4: Record session journal + +```bash +python3 ./.trellis/scripts/add_session.py \ + --title "Session Title" \ + --commit "hash1,hash2" \ + --summary "Brief summary" +``` + +Use the work-commit hashes produced in Phase 3.4 (visible in Step 1's `Recent commits` list, or via `git log --oneline`) for `--commit`. Do not include the archive commit hashes from Step 3. This produces a `chore: record journal` commit. + +Final git log order: `<work commits from 3.4>` → `chore(task): archive ...` (one or more) → `chore: record journal`. diff --git a/.agents/skills/trellis-meta/SKILL.md b/.agents/skills/trellis-meta/SKILL.md new file mode 100644 index 0000000..0754b53 --- /dev/null +++ b/.agents/skills/trellis-meta/SKILL.md @@ -0,0 +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, 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, bundled runtime agents, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.reasonix/`, `.kilocode/`, `.agent/`, `.devin/`, 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. +- 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` — 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 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 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 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 15 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, 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; 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` 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/add-project-local-conventions.md b/.agents/skills/trellis-meta/references/customize-local/add-project-local-conventions.md new file mode 100644 index 0000000..608aaa6 --- /dev/null +++ b/.agents/skills/trellis-meta/references/customize-local/add-project-local-conventions.md @@ -0,0 +1,83 @@ +# Add Project-Local Conventions + +Often the user does not need to change Trellis mechanics; they need local AI to understand their team's conventions. In that case, prefer `.trellis/spec/` or a project-local skill instead of editing `trellis-meta`. + +## Where To Put Things + +| Content type | Location | +| --- | --- | +| Rules code must follow | `.trellis/spec/<layer>/` | +| Cross-layer thinking methods | `.trellis/spec/guides/` | +| AI capability for a project-specific flow | Platform-local skill | +| One-off task material | `.trellis/tasks/<task>/` | +| Session summary | `.trellis/workspace/<developer>/journal-N.md` | + +## Create A Project-Local Skill + +If the user wants AI to know "how this project customizes Trellis," create a local skill: + +```text +.claude/skills/trellis-local/ +└── SKILL.md +``` + +Example: + +```md +--- +name: trellis-local +description: "Project-local Trellis customizations for this repository. Use when changing this project's Trellis workflow, hooks, local agents, or team-specific conventions." +--- + +# Trellis Local + +## Local Scope + +This skill documents this repository's Trellis customizations only. + +## Custom Workflow Rules + +- ... + +## Local Hook Changes + +- ... + +## Local Agent Changes + +- ... +``` + +For multi-platform projects, place equivalent versions in other platform skill directories, or use `.agents/skills/` for platforms that support the shared layer. + +## Write To `.trellis/spec/` + +If the content is a coding convention, write it to spec. Examples: + +```text +.trellis/spec/backend/error-handling.md +.trellis/spec/frontend/components.md +.trellis/spec/guides/cross-platform-thinking-guide.md +``` + +After writing it, update the corresponding `index.md` so AI can find the new rule from the entry point. + +## Make The Current Task Use New Conventions + +After writing a spec, add it to the current task context: + +```bash +python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/backend/error-handling.md" "Error handling conventions" +python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/backend/error-handling.md" "Review error handling" +``` + +## Do Not Store Project-Private Rules In `trellis-meta` + +`trellis-meta` is a public skill for understanding Trellis architecture and local customization entry points. Put project-private content in: + +- `.trellis/spec/` +- a project-local skill +- the current task +- workspace journal + +This prevents future updates to Trellis's built-in `trellis-meta` from overwriting the team's own conventions. diff --git a/.agents/skills/trellis-meta/references/customize-local/change-agents.md b/.agents/skills/trellis-meta/references/customize-local/change-agents.md new file mode 100644 index 0000000..88e7376 --- /dev/null +++ b/.agents/skills/trellis-meta/references/customize-local/change-agents.md @@ -0,0 +1,56 @@ +# Change Local Agents + +When the user wants to change `trellis-research`, `trellis-implement`, or `trellis-check` behavior, edit platform agent files in the user project. + +## Read These Files First + +1. Target platform agent directory +2. `.trellis/workflow.md` Phase 2 / research routing +3. Current task `prd.md` +4. Current task `implement.jsonl` / `check.jsonl` +5. Relevant hook or agent prelude + +## Common Paths + +| Platform | Path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/cli/agents/trellis-*.md` | + +Use the actual paths in the user project as authoritative. + +## Common Needs + +| Need | Which agent to edit | +| --- | --- | +| Research must write files, not only reply in chat | `trellis-research` | +| Certain local specs must be read before implementation | `trellis-implement` + `implement.jsonl` configuration rules | +| Specific commands must run during checking | `trellis-check` | +| Agent must not modify certain directories | The corresponding agent's write boundary instructions | +| Agent output format must be fixed | The corresponding agent's final/reporting instructions | + +## Modification Principles + +1. **Preserve role boundaries**: research investigates and persists; implement writes implementation; check reviews and fixes. +2. **Do not hard-code project specs into agents**: long-term specs belong in `.trellis/spec/`; agents are responsible for reading them. +3. **Make read order explicit**: active task -> PRD -> info -> JSONL -> spec/research. +4. **Make write boundaries explicit**: which directories may be written and which may not. +5. **Synchronize across platforms**: when the user configured multiple platforms, decide whether to change only the current platform or all platform agents. + +## Agent Pull Platforms + +If an agent file contains a prelude for "read task/context after startup," do not remove those steps when editing. Otherwise the agent will work only from chat context and bypass Trellis's core mechanism. + +## Hook Push Platforms + +If context is injected by a hook, the agent file should still retain responsibility boundaries. Do not remove PRD/spec requirements from the agent just because a hook injects context. 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 new file mode 100644 index 0000000..002a259 --- /dev/null +++ b/.agents/skills/trellis-meta/references/customize-local/change-context-loading.md @@ -0,0 +1,84 @@ +# Change Local Context Loading + +Context loading determines when AI reads workflow, task, spec, research, workspace, and git status. Read this page when the user says "AI does not know the current task," "the agent did not read specs," or "there is too much/too little context." + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/scripts/get_context.py` +3. `.trellis/scripts/common/session_context.py` +4. `.trellis/scripts/common/task_context.py` +5. `.trellis/scripts/common/active_task.py` +6. Current platform hooks or agent files +7. The current task's `implement.jsonl` / `check.jsonl` + +## Context Sources + +| Source | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow and next-action hints. | +| `.trellis/tasks/<task>/prd.md` | Current task requirements. | +| `.trellis/tasks/<task>/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. | +| `.trellis/workspace/` | Session records. | +| git status | Current working tree changes. | + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Inject more/less information in new sessions | `session_context.py` or the platform `session-start` hook. | +| Change hints on each user input | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The `inject-workflow-state` hook is parser-only and reads the block verbatim. | +| Agent did not read specs | Task JSONL, agent prelude, `inject-subagent-context` hook. | +| Active task is lost | `active_task.py` and platform session identity propagation. | +| Change JSONL validation rules | `task_context.py`. | + +## JSONL Rules + +`implement.jsonl` / `check.jsonl` are the key context loading interface: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-x/research/api.md", "reason": "API research"} +``` + +Include only spec/research files. Do not put code files that will be modified into these manifests; agents read code files themselves during implementation. + +## Change Session Context + +If the user wants every new session to see more project state, edit: + +- `.trellis/scripts/common/session_context.py` +- the corresponding platform `session-start` hook + +Context cannot grow without bound. Prefer injecting indexes and paths so the AI can read detailed files on demand. + +## Change Sub-Agent Context + +First determine which mode the platform uses: + +- hook push: edit the `inject-subagent-context` hook. +- agent pull: edit the read steps in the corresponding `trellis-implement` / `trellis-check` agent file. + +In both modes, make sure the agent ultimately reads: + +1. active task +2. 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 + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py list-context <task> +python3 ./.trellis/scripts/task.py validate <task> +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Confirm the task and JSONL are correct before editing hooks/agents. diff --git a/.agents/skills/trellis-meta/references/customize-local/change-hooks.md b/.agents/skills/trellis-meta/references/customize-local/change-hooks.md new file mode 100644 index 0000000..79aa5c5 --- /dev/null +++ b/.agents/skills/trellis-meta/references/customize-local/change-hooks.md @@ -0,0 +1,57 @@ +# Change Local Hooks + +Hooks are the automation layer that connects a platform to Trellis. When the user wants to change "when context is injected," "how shell commands inherit a session," or "which files are read before an agent starts," hooks are usually the edit point. + +## Read These Files First + +1. Target platform settings/config, such as `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`, `.trae/hooks.json` +2. Target platform hooks directory +3. `.trellis/scripts/common/active_task.py` +4. `.trellis/scripts/common/session_context.py` +5. `.trellis/workflow.md` + +## Common Hook Types + +| Hook | Purpose | +| --- | --- | +| session-start | Injects a Trellis overview when a session starts, clears, or compacts. | +| workflow-state | Injects a state hint on each user input. | +| sub-agent context | Injects PRD/spec/research before an agent starts. | +| shell session bridge | Lets `task.py` commands in shell see the same session identity. | + +## Modification Steps + +1. Find the hook registration in settings/config. +2. Confirm the registered script path exists. +3. Read the hook script and identify inputs, outputs, and called `.trellis/scripts/`. +4. Modify hook behavior. +5. If the hook depends on workflow content, synchronize `.trellis/workflow.md`. + +## Example: Change New-Session Injection Content + +First find the session-start hook: + +```text +.claude/settings.json +.claude/hooks/session-start.py +``` + +If the hook ultimately calls `.trellis/scripts/get_context.py` or `session_context.py`, editing the local script is usually more robust than hard-coding content in the hook. + +## Example: Agent Did Not Read JSONL + +First confirm: + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py validate <task> +``` + +If the task and JSONL are correct, determine whether the platform uses hook push or agent pull. For hook push, edit `inject-subagent-context`; for agent pull, edit the agent file. + +## Notes + +- Settings handle registration, hook scripts handle behavior; inspect both together. +- Different platforms support different hook events. Do not directly copy another platform's settings. +- Hooks should read project-local `.trellis/`; they should not depend on Trellis upstream source paths. +- Hook failures should produce visible errors so AI does not silently lose context. 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 new file mode 100644 index 0000000..0ed8792 --- /dev/null +++ b/.agents/skills/trellis-meta/references/customize-local/change-skills-or-commands.md @@ -0,0 +1,123 @@ +# Change Local Skills, Commands, Prompts, And Workflows + +When the user wants to change AI entry points, auto-trigger rules, or explicit command behavior, edit skills, commands, prompts, or workflows in local platform directories. + +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 + +| Goal | Recommendation | +| --- | --- | +| AI should automatically know a capability | Add or modify a skill. | +| User wants to trigger manually with a command | Add or modify a command/prompt/workflow. | +| Team project conventions | Prefer `.trellis/spec/` or a project-local skill — 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 + +A skill is usually: + +```text +<skill-name>/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should be short and responsible for triggering/routing. Put long content in `references/` so AI can read it on demand. + +The frontmatter description should specify when to use the skill. Example: + +```yaml +description: "Use when customizing this project's deployment workflow and release checklist." +``` + +Do not write vague descriptions such as "helpful project skill"; they can trigger incorrectly. + +### 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: + +- How the user triggers it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +If a command only repeats workflow rules, prefer making it reference/read `.trellis/workflow.md` instead of maintaining a second copy of the flow. + +## Common Paths + +| Platform | Entry directories | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| 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/` | +| Factory Droid | `.factory/skills/`, `.factory/commands/` | +| Pi Agent | `.pi/skills/` | +| Reasonix | `.reasonix/skills/` (no separate commands dir; slash commands built into the platform) | +| ZCode | `.agents/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 — 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 (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 new file mode 100644 index 0000000..ee9a176 --- /dev/null +++ b/.agents/skills/trellis-meta/references/customize-local/change-spec-structure.md @@ -0,0 +1,83 @@ +# Change Local Spec Structure + +When the user wants to change the engineering conventions AI follows, add new spec layers, or adjust monorepo package mapping, edit `.trellis/spec/` and `.trellis/config.yaml`. + +## Read These Files First + +1. `.trellis/config.yaml` +2. `.trellis/spec/` +3. `.trellis/workflow.md` planning artifact guidance and Phase 3.3 +4. Current task `implement.jsonl` / `check.jsonl` + +## Common Needs + +| Need | Edit location | +| --- | --- | +| Add backend/frontend/docs/test spec layer | `.trellis/spec/<layer>/` or `.trellis/spec/<package>/<layer>/` | +| Add shared thinking guides | `.trellis/spec/guides/` | +| Adjust monorepo packages | `packages` in `.trellis/config.yaml` | +| Change default package | `default_package` in `.trellis/config.yaml` | +| Control spec scanning scope | `spec_scope` in `.trellis/config.yaml` | +| Make a task read a new spec | Task `implement.jsonl` / `check.jsonl` | + +## Add A Spec Layer + +Single-repository example: + +```text +.trellis/spec/security/ +├── index.md +└── auth.md +``` + +Monorepo example: + +```text +.trellis/spec/webapp/security/ +├── index.md +└── auth.md +``` + +`index.md` should include: + +- What code this layer applies to. +- Pre-Development Checklist. +- Quality Check. +- Links to specific guideline files. + +## Update Context + +Adding a spec does not mean every task automatically reads it. The current task must reference it in JSONL: + +```bash +python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/webapp/security/index.md" "Security conventions" +python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/webapp/security/index.md" "Security review rules" +``` + +## Change Monorepo Packages + +Example `.trellis/config.yaml`: + +```yaml +packages: + webapp: + path: apps/web + api: + path: apps/api +default_package: webapp +``` + +After editing, run: + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Use this output to confirm AI can see the correct packages and spec layers. + +## Notes + +- Specs are user project conventions and can be changed according to project needs. +- Do not put temporary task information into specs; put temporary information in the task. +- Do not put long-term conventions only in agents or commands; preserve them in specs. +- After changing spec structure, check whether existing task JSONL files still point to files that exist. diff --git a/.agents/skills/trellis-meta/references/customize-local/change-task-lifecycle.md b/.agents/skills/trellis-meta/references/customize-local/change-task-lifecycle.md new file mode 100644 index 0000000..a7a340f --- /dev/null +++ b/.agents/skills/trellis-meta/references/customize-local/change-task-lifecycle.md @@ -0,0 +1,90 @@ +# Change Local Task Lifecycle + +Task lifecycle includes creation, start, context configuration, finish, archive, parent/child tasks, and lifecycle hooks. The default customization targets are `.trellis/tasks/`, `.trellis/config.yaml`, and `.trellis/scripts/`. + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/config.yaml` +3. `.trellis/scripts/task.py` +4. `.trellis/scripts/common/task_store.py` +5. `.trellis/scripts/common/task_utils.py` +6. The current task's `.trellis/tasks/<task>/task.json` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Automatically sync an external system after task creation | `hooks.after_create` in `.trellis/config.yaml`. | +| Automatically update status after task start | `hooks.after_start` in `.trellis/config.yaml`. | +| Run a script after task finish | `hooks.after_finish` in `.trellis/config.yaml`. | +| Clean external resources after archive | `hooks.after_archive` in `.trellis/config.yaml`. | +| Change default task fields | `.trellis/scripts/common/task_store.py`. | +| Change task parsing/search | `.trellis/scripts/common/task_utils.py`. | +| Change active task behavior | `.trellis/scripts/common/active_task.py`. | + +## lifecycle hooks + +`.trellis/config.yaml` supports: + +```yaml +hooks: + after_create: + - "python3 .trellis/scripts/hooks/my_sync.py create" + after_start: + - "python3 .trellis/scripts/hooks/my_sync.py start" + after_finish: + - "python3 .trellis/scripts/hooks/my_sync.py finish" + after_archive: + - "python3 .trellis/scripts/hooks/my_sync.py archive" +``` + +Hook commands receive the `TASK_JSON_PATH` environment variable, pointing to the current task's `task.json`. Hook failures should usually warn, but not block the main task operation. + +## Change Task Fields + +If the user wants to add project-local fields, prefer putting them under `meta` in `task.json` to avoid breaking existing scripts' assumptions about standard fields. + +Example: + +```json +"meta": { + "linearIssue": "ENG-123", + "risk": "high" +} +``` + +If standard fields really need to change, inspect every local script that reads `task.json`. + +## Change Active Task + +Active task is session-level state stored in `.trellis/.runtime/sessions/`. Do not fall back to a global `.current-task` model. If the user wants to change active task behavior, edit: + +- `.trellis/scripts/common/active_task.py` +- platform hooks or shell session bridges +- active task descriptions in `.trellis/workflow.md` + +### `task.py create` Sets the Active Pointer + +`cmd_create` in `.trellis/scripts/common/task_store.py` calls `set_active_task` best-effort right after writing the new task directory. The behavior: + +- When the calling shell carries session identity (`TRELLIS_CONTEXT_ID` env var, or any platform-specific session env that `resolve_context_key` recognizes — see `active_task.py:_ENV_SESSION_KEYS`), the per-session pointer at `.trellis/.runtime/sessions/<context_key>.json` is rewritten to point at the new task. The task's `status=planning` and `[workflow-state:planning]` fires on the very next `UserPromptSubmit`. +- When session identity is unavailable (raw CLI invocation outside an AI session, or a platform that doesn't propagate identity to shell), the task directory is still created and `status=planning` is still written, but the active pointer is left untouched. The user can attach the task later with `task.py start <dir>` once they're back in an AI session. + +This makes `[workflow-state:planning]` the live breadcrumb during the brainstorm and JSONL curation work that follows `task.py create`. The pre-R7 behavior left the breadcrumb stuck on `no_task` until `task.py start`, so the planning block was effectively dead text. + +If you fork `task.py` to add a new creation path (e.g. an external import that bypasses `cmd_create`), audit whether your path also calls `set_active_task`. Without that call, your created tasks will not surface as active. The full status writer table is in `.trellis/spec/cli/backend/workflow-state-contract.md`. + +## Modification Steps + +1. Confirm the current task with `python3 ./.trellis/scripts/task.py current --source`. +2. Read the current task's `task.json` and confirm status and fields. +3. For configuration needs, edit `.trellis/config.yaml` first. +4. For script behavior needs, then edit `.trellis/scripts/`. +5. If the AI flow changed, synchronize `.trellis/workflow.md`. + +## Do Not + +- Do not directly edit `.trellis/.runtime/sessions/` to "fix" business state. +- Do not hard-code project-private fields into scripts; prefer `meta`. +- Do not default to asking the user to fork Trellis CLI. diff --git a/.agents/skills/trellis-meta/references/customize-local/change-workflow.md b/.agents/skills/trellis-meta/references/customize-local/change-workflow.md new file mode 100644 index 0000000..337c985 --- /dev/null +++ b/.agents/skills/trellis-meta/references/customize-local/change-workflow.md @@ -0,0 +1,65 @@ +# Change Local Workflow + +When the user wants to change Trellis phases, next-action hints, whether to create tasks, whether to use sub-agents, or when to check/wrap up, edit `.trellis/workflow.md` first. + +## Read These Files First + +1. `.trellis/workflow.md` +2. Entry files for the current platform, such as skills/commands/prompts/workflows +3. The current task's `task.json` and `prd.md` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Change phase names or phase order | `Phase Index` and the corresponding Phase sections. | +| Change whether to create a task when there is no task | `[workflow-state:no_task]` state block. | +| Change the next step during planning | Phase 1 and `[workflow-state:planning]`. | +| Change whether an agent is required during in_progress | Phase 2 and `[workflow-state:in_progress]`. | +| Change wrap-up after completion | Phase 3 and `[workflow-state:completed]`. | +| Change which skill a user intent triggers | `Skill Routing` table. | + +## Modification Steps + +1. Find the relevant section in `.trellis/workflow.md`. +2. When changing rules, keep explicit trigger conditions and next actions. +3. If adding or renaming a skill/agent, synchronize the corresponding files in platform directories. +4. Workflow-state changes only need an edit to the `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook is parser-only — it reads whatever you put in the block. Keep the opening and closing tags' STATUS strings identical (`[workflow-state:foo]…[/workflow-state:foo]`); mismatched STATUS pairs are silently dropped. +5. Make the AI reread `.trellis/workflow.md`; do not keep using rules from the old conversation. + +## Example: Relax Task Creation Requirements + +To change when task creation can be skipped, usually edit `[workflow-state:no_task]`: + +```md +[workflow-state:no_task] +Task is not required when the answer is a one-reply explanation, no files are changed, and no research is needed. +[/workflow-state:no_task] +``` + +If the formal Phase 1 flow also needs to change, synchronize the Phase 1 section. + +## Example: One Platform Does Not Use Sub-Agents + +If the user wants only one platform to avoid sub-agents, first confirm whether that platform has a separate group in the workflow. Then change Phase 2 routing for that platform group instead of deleting all `trellis-implement` / `trellis-check` instructions across platforms. + +## `/trellis:continue` Route Table + +`/trellis:continue` resumes a task by deciding which phase step to load next. The decision combines `task.json.status` with the presence of artifacts inside the task directory. The mapping is fixed in the command itself; forks that add custom statuses must extend both the workflow.md tag block and this table. + +| `status` | Artifact state | Resume at | +| --- | --- | --- | +| `planning` | `prd.md` missing | Phase 1.1 (load `trellis-brainstorm`) | +| `planning` | 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.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. + +## Notes + +`.trellis/workflow.md` is the local project workflow, not an immutable template. The user can adapt it to team habits. After editing it, platform entry files may still contain old descriptions, so inspect them too. diff --git a/.agents/skills/trellis-meta/references/customize-local/overview.md b/.agents/skills/trellis-meta/references/customize-local/overview.md new file mode 100644 index 0000000..b75d208 --- /dev/null +++ b/.agents/skills/trellis-meta/references/customize-local/overview.md @@ -0,0 +1,55 @@ +# Local Customization Overview + +This directory is for local AI working in a user project where Trellis was installed through npm and `trellis init` has already been run. The AI should modify generated `.trellis/` and platform directories inside the project, not Trellis CLI upstream source code. + +## First Determine What The User Actually Wants To Change + +| User wording | Read first | +| --- | --- | +| "Change the Trellis flow / phases / next prompt" | `change-workflow.md` | +| "Change task creation, status, archive, or hooks" | `change-task-lifecycle.md` | +| "AI did not read context / change injected content" | `change-context-loading.md` | +| "A platform hook is not behaving as expected" | `change-hooks.md` | +| "Change implement/check/research agent behavior" | `change-agents.md` | +| "Add a skill/command/workflow/prompt" | `change-skills-or-commands.md` | +| "Adjust the project spec structure" | `change-spec-structure.md` | +| "Add team conventions and local notes" | `add-project-local-conventions.md` | + +## General Operation Order + +1. **Confirm platform and directories**: inspect which directories exist, such as `.claude/`, `.codex/`, `.cursor/`, `.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. +5. **Synchronize semantics**: if a shared flow changes, check whether platform entry points also need changes; if a platform entry changes, check whether `.trellis/workflow.md` still agrees. + +## Local File Priority + +| Layer | Files | +| --- | --- | +| Workflow | `.trellis/workflow.md` | +| Project configuration | `.trellis/config.yaml` | +| Task material | `.trellis/tasks/<task>/` | +| Project specs | `.trellis/spec/` | +| Runtime scripts | `.trellis/scripts/` | +| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.zcode/`, and similar directories | +| Shared skill | `.agents/skills/` | + +## Things Not To Do By Default + +- Do not edit the global npm install directory. +- Do not edit `node_modules/@mindfoldhq/trellis`. +- Do not assume the user has the Trellis GitHub repository. +- Do not overwrite local files already modified by the user with default templates. +- Do not put team project rules into public `trellis-meta`; project rules belong in `.trellis/spec/` or a local skill. + +## When To Inspect Upstream Source + +Switch to an upstream source-code perspective only when the user explicitly expresses one of these goals: + +- "I want to open a PR to Trellis" +- "I want to change npm package publish contents" +- "I want to fork Trellis" +- "I want to modify the generation logic for `trellis init/update`" + +Otherwise, default to modifying local Trellis files inside the user project. 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 0000000..1c819cb --- /dev/null +++ b/.agents/skills/trellis-meta/references/local-architecture/bundled-skills.md @@ -0,0 +1,146 @@ +# 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` | +| 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`, 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 new file mode 100644 index 0000000..4a7517b --- /dev/null +++ b/.agents/skills/trellis-meta/references/local-architecture/context-injection.md @@ -0,0 +1,68 @@ +# Local Context Injection System + +Trellis context injection aims to make AI read the right files at the right time instead of relying on model memory. In a user project, injection is implemented by `.trellis/` scripts together with platform hooks, agents, and skills. + +## Injected Context Types + +| Type | Source | Purpose | +| --- | --- | --- | +| session context | `.trellis/scripts/get_context.py` | Current developer, git status, active task, active tasks, journal, packages. | +| workflow context | `.trellis/workflow.md` | Current Trellis flow and next action. | +| spec context | `.trellis/spec/` + task JSONL | Specs that must be followed during implementation/checking. | +| task context | `.trellis/tasks/<task>/prd.md`, `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 + +Platforms with session-start support inject a Trellis overview when a session starts, clears, compacts, or receives a similar event. Injected content usually includes: + +- workflow summary. +- current task status. +- active tasks. +- spec index paths. +- developer identity and git status. + +If the user feels the AI does not know the current task in a new session, first check whether the platform's session-start hook or equivalent mechanism is installed and running. + +## workflow-state + +workflow-state is a lightweight hint injected around each user turn. Based on current task status, it selects a block from `.trellis/workflow.md`, such as `no_task`, `planning`, `in_progress`, or `completed`. + +If the user wants to change "what the AI should do next in a given state," edit the corresponding state block in `.trellis/workflow.md` first. + +## sub-agent context + +Implement and check agents need task context. Trellis has two loading modes: + +1. **hook push**: a platform hook injects 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 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 + +`implement.jsonl` and `check.jsonl` contain one JSON object per line: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend rules"} +``` + +Readers should skip seed rows without a `file` field. When configuring JSONL, the AI should include only spec/research files, not pre-register code files that will be modified. + +## Active Task And Context Key + +Active task state lives in `.trellis/.runtime/sessions/` and is isolated per session. Hooks try to resolve the context key from platform events, environment variables, transcript paths, or `TRELLIS_CONTEXT_ID`. + +If shell commands cannot see the same context key, `task.py current --source` may report no active task. In that case, check whether the platform passes session identity into the shell instead of hand-writing a global current-task file. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change session-start injected content | The platform's `session-start` hook or plugin file. | +| Change per-turn workflow-state rules | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The platform workflow-state hook parses these blocks verbatim and embeds no fallback text. | +| Change how sub-agents read context | Platform agent definitions, the `inject-subagent-context` hook, or agent preludes. | +| Change JSONL validation/display | `.trellis/scripts/common/task_context.py`. | +| Change active task resolution | `.trellis/scripts/common/active_task.py`. | + +When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct 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 new file mode 100644 index 0000000..5e53865 --- /dev/null +++ b/.agents/skills/trellis-meta/references/local-architecture/generated-files.md @@ -0,0 +1,80 @@ +# Local Files Generated After Init + +`trellis init` writes the Trellis runtime into the user project. Later, `trellis update` tries to update Trellis-managed template files, but it uses `.trellis/.template-hashes.json` to determine which files have already been modified by the user. + +This page only describes files that are visible and editable inside the user project. + +## `.trellis/` + +```text +.trellis/ +├── workflow.md +├── config.yaml +├── .developer +├── .version +├── .template-hashes.json +├── .runtime/ +├── scripts/ +├── spec/ +├── tasks/ +└── workspace/ +``` + +| Path | Usually editable? | Notes | +| --- | --- | --- | +| `.trellis/workflow.md` | Yes | Local workflow documentation and AI routing rules. | +| `.trellis/config.yaml` | Yes | Project configuration, hooks, packages, journal line limits, and related settings. | +| `.trellis/spec/` | Yes | Project specs, intended to be updated regularly by users and AI. | +| `.trellis/tasks/` | Yes | Task material and research artifacts, maintained by the task workflow. | +| `.trellis/workspace/` | Yes | Session records, usually written by `add_session.py`. | +| `.trellis/scripts/` | Carefully | Local runtime. It can be customized, but only after understanding the call chain. | +| `.trellis/.runtime/` | No | Runtime state, usually written automatically by hooks/scripts. | +| `.trellis/.developer` | Carefully | Current developer identity. | +| `.trellis/.version` | No | Trellis version record used by update/migration logic. | +| `.trellis/.template-hashes.json` | No | Template hash record. Do not hand-write business rules here. | + +## Platform Directories + +Different platforms generate different directories. Common categories: + +| Category | Example paths | Purpose | +| --- | --- | --- | +| hooks | `.claude/hooks/`, `.codex/hooks/`, `.cursor/hooks/` | Inject session context, workflow-state, and sub-agent context. | +| settings | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.trae/hooks.json` | Tell the platform when to run hooks or plugins. | +| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/`, `.zcode/cli/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/`, `.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. + +## Meaning Of Template Hashes + +`.trellis/.template-hashes.json` records the content hash from the last time Trellis wrote a template file. `trellis update` uses it to distinguish three cases: + +| Case | Update behavior | +| --- | --- | +| File was not modified by the user | It can be updated automatically. | +| File was modified by the user | Prompt the user to overwrite, keep, or generate `.new`. | +| File is no longer a current template | It may be deleted, renamed, or preserved according to migration rules. | + +When an AI customizes local Trellis files, it does not need to maintain hashes manually. It is normal for Trellis update to recognize the result as "modified by the user." + +## Local Customization Boundaries + +Editable by default: + +- `.trellis/workflow.md` +- `.trellis/config.yaml` +- `.trellis/spec/**` +- `.trellis/scripts/**` +- Platform hooks, settings, agents, skills, commands, prompts, and workflows + +Do not edit by default: + +- Global npm install directory +- `node_modules/@mindfoldhq/trellis` +- Trellis GitHub repository source code +- Concrete state files under `.trellis/.runtime/**` +- Hash contents inside `.trellis/.template-hashes.json` + +Switch to the Trellis CLI source-code perspective only when the user explicitly wants to contribute upstream. 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 0000000..6df61eb --- /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 new file mode 100644 index 0000000..e97cab8 --- /dev/null +++ b/.agents/skills/trellis-meta/references/local-architecture/overview.md @@ -0,0 +1,51 @@ +# Local Trellis Architecture Overview + +`trellis-meta` is for user projects that have already run `trellis init`. The user's machine usually has only the npm-installed `trellis` command plus the Trellis files generated inside the project; it may not have the Trellis CLI source code. + +Therefore, when an AI uses this skill, the default customization target is local files inside the user project: + +- `.trellis/`: workflow, tasks, specs, memory, scripts, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.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. + +## Local System Model + +Trellis provides three layers inside a user project: + +1. **Workflow layer**: `.trellis/workflow.md` defines phases, routing, next actions, and prompt blocks. +2. **Persistence layer**: `.trellis/tasks/`, `.trellis/spec/`, and `.trellis/workspace/` store tasks, specs, and session memory. +3. **Platform integration layer**: hooks, settings, agents, skills, commands, prompts, and workflows in platform directories connect the Trellis workflow to different AI tools. + +All three layers live inside the user project, so an AI can read and modify them directly. + +## Core Paths + +| Path | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow phases, skill routing, and workflow-state prompt blocks. | +| `.trellis/config.yaml` | Project configuration, task lifecycle hooks, monorepo package configuration, and journal configuration. | +| `.trellis/spec/` | The user's project-specific coding conventions and thinking guides. | +| `.trellis/tasks/` | Each task's PRD, technical notes, research files, and JSONL context. | +| `.trellis/workspace/` | Per-developer journals and cross-session memory. | +| `.trellis/scripts/` | Local Python runtime used by commands, hooks, and context injection. | +| `.trellis/.runtime/` | Session-level runtime state, such as the current task pointer. | +| `.trellis/.template-hashes.json` | Template hashes for Trellis-managed files, used by update to determine whether local files were modified by the user. | + +## AI Customization Principles + +1. **Find the local source of truth first**: Do not edit from memory. Read `.trellis/workflow.md`, `.trellis/config.yaml`, the relevant platform directory, and related task files first. +2. **Edit the user project, not the npm package cache**: Modify generated files inside the project, not `node_modules` or the global npm install directory. +3. **Keep platform files aligned with `.trellis/`**: If workflow routing changes, also check whether platform skills or commands still describe the same flow. +4. **Put project-specific rules in `.trellis/spec/` or a local skill**: Do not put team conventions into `trellis-meta`. +5. **Preserve user changes**: If a file was already modified locally, work from the current content instead of overwriting it with a default template. + +## How To Use This Directory + +- To understand which files exist after init, read `generated-files.md`. +- To change phases, routing, or next actions, read `workflow.md`. +- To change the task model, JSONL context, or active task behavior, read `task-system.md`. +- To change coding convention injection, read `spec-system.md`. +- To understand journals and cross-session memory, read `workspace-memory.md`. +- To change hooks or sub-agent context loading, read `context-injection.md`. diff --git a/.agents/skills/trellis-meta/references/local-architecture/spec-system.md b/.agents/skills/trellis-meta/references/local-architecture/spec-system.md new file mode 100644 index 0000000..38fdf14 --- /dev/null +++ b/.agents/skills/trellis-meta/references/local-architecture/spec-system.md @@ -0,0 +1,102 @@ +# Local Spec System + +`.trellis/spec/` is the user's project-specific engineering spec library. Trellis is not about making AI memorize conventions; it injects relevant specs or requires the AI to read them at the right time. + +## Directory Model + +A common single-repository structure: + +```text +.trellis/spec/ +├── backend/ +│ ├── index.md +│ └── ... +├── frontend/ +│ ├── index.md +│ └── ... +└── guides/ + ├── index.md + └── ... +``` + +A common monorepo structure: + +```text +.trellis/spec/ +├── cli/ +│ ├── backend/ +│ │ ├── index.md +│ │ └── ... +│ └── unit-test/ +│ ├── index.md +│ └── ... +├── docs-site/ +│ └── docs/ +│ ├── index.md +│ └── ... +└── guides/ + ├── index.md + └── ... +``` + +`index.md` is the entry point for each layer. It should list the Pre-Development Checklist and Quality Check. Specific guidelines live in other Markdown files in the same directory. + +## Package Configuration + +`.trellis/config.yaml` can declare packages: + +```yaml +packages: + cli: + path: packages/cli + docs-site: + path: docs-site + type: submodule +default_package: cli +``` + +The AI can run: + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +This command lists packages and spec layers for the current project. Use this output as the reference when configuring context JSONL. + +## How Specs Enter Tasks + +Before a task enters implementation, 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"} +{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Test expectations"} +``` + +Sub-agents or platform preludes read these JSONL files and load the referenced specs. On platforms without sub-agent support, the AI should read the relevant specs directly according to the workflow. + +## What Specs Should Contain + +Specs should contain executable engineering conventions for the project, not generic best practices: + +- Where files should live. +- How error handling should be expressed. +- Input/output contracts for APIs, hooks, and commands. +- Patterns that are forbidden. +- Cases that require tests. +- Project-specific pitfalls and how to avoid them. + +When the AI learns a new rule during implementation or debugging, it should update `.trellis/spec/` rather than only summarizing it in chat. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Add a new spec layer | `.trellis/spec/<package>/<layer>/index.md` and corresponding guideline files. | +| Change monorepo spec mapping | `packages` / `default_package` / `spec_scope` in `.trellis/config.yaml`. | +| Change which specs AI reads before implementation | The task's `implement.jsonl`. | +| Change which specs AI reads during checking | The task's `check.jsonl`. | +| Change when specs should be updated | Phase 3.3 in `.trellis/workflow.md` and the `trellis-update-spec` skill. | + +## Boundaries + +`.trellis/spec/` is the user's project specification, not a permanent copy of Trellis built-in templates. The AI should encourage the user to update it according to the actual project code instead of treating Trellis default templates as immutable documents. diff --git a/.agents/skills/trellis-meta/references/local-architecture/task-system.md b/.agents/skills/trellis-meta/references/local-architecture/task-system.md new file mode 100644 index 0000000..7133495 --- /dev/null +++ b/.agents/skills/trellis-meta/references/local-architecture/task-system.md @@ -0,0 +1,130 @@ +# Local Task System + +The Trellis task system is stored entirely under `.trellis/tasks/` in the user project. Each task is a directory containing requirements, context, research, state, and relationship information. + +## Task Directory Structure + +```text +.trellis/tasks/ +├── 04-28-example-task/ +│ ├── task.json +│ ├── prd.md +│ ├── design.md +│ ├── implement.md +│ ├── implement.jsonl +│ ├── check.jsonl +│ └── research/ +└── archive/ + └── 2026-04/ +``` + +| File | Purpose | +| --- | --- | +| `task.json` | Task metadata: status, assignee, priority, branch, parent/child tasks, and similar fields. | +| `prd.md` | Requirements, 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. | + +## `task.json` + +`task.json` records task status and metadata. Common fields: + +| Field | Meaning | +| --- | --- | +| `id` / `name` / `title` | Task identity and title. | +| `status` | Status such as `planning`, `in_progress`, `review`, or `completed`. | +| `priority` | `P0`, `P1`, `P2`, `P3`. | +| `creator` / `assignee` | Creator and assignee. | +| `package` | Target package in a monorepo; may be empty. | +| `branch` / `base_branch` | Working branch and PR target branch. | +| `children` / `parent` | Parent/child task relationships. | +| `commit` / `pr_url` | Commit and PR information after completion. | +| `meta` | Extension fields. | + +## 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 + +The user sees a "current task," but Trellis stores active task state per session. + +```text +.trellis/.runtime/sessions/<context-key>.json +``` + +`task.py start` writes the task path into the runtime session file for the current session. `task.py current --source` shows the current task and where it came from. Different AI windows can point to different tasks without overwriting each other. + +If the platform or shell environment has no stable session identity, `task.py start` may be unable to set the active task. The AI should read the error, inspect the platform hook/session environment, and not fall back to a shared global pointer. + +## JSONL Context + +`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first. They do not replace `implement.md`; `implement.md` is the human-readable execution plan. + +Format: + +```jsonl +{"file": ".trellis/spec/cli/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-example/research/api.md", "reason": "API research"} +``` + +Rules: + +- Include spec and research files. +- Do not include code files that are about to be modified. +- Do not treat temporary conclusions in chat as the only context. +- Seed rows have no `file` field; they only prompt the AI to fill in real entries. + +## Common Commands + +```bash +python3 ./.trellis/scripts/task.py create "<title>" --slug <slug> +python3 ./.trellis/scripts/task.py start <task> +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py add-context <task> implement <file> <reason> +python3 ./.trellis/scripts/task.py validate <task> +python3 ./.trellis/scripts/task.py finish +python3 ./.trellis/scripts/task.py archive <task> +``` + +When modifying the task system, the AI should prefer script commands to maintain structure. Edit JSON/Markdown directly only when scripts do not cover the need. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change the default task template | `.trellis/scripts/common/task_store.py` and task creation instructions. | +| Change status semantics | `.trellis/workflow.md`, workflow-state hook logic, and task usage conventions. | +| Add task lifecycle actions | `hooks.after_*` in `.trellis/config.yaml`. | +| Change context rules | 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/local-architecture/workflow.md b/.agents/skills/trellis-meta/references/local-architecture/workflow.md new file mode 100644 index 0000000..f0659ff --- /dev/null +++ b/.agents/skills/trellis-meta/references/local-architecture/workflow.md @@ -0,0 +1,75 @@ +# Local Workflow System + +`.trellis/workflow.md` is the Trellis workflow source of truth inside the user project. An AI does not need Trellis source code to understand how the current project should move tasks forward; this file is enough. + +## File Responsibilities + +`.trellis/workflow.md` has three responsibilities: + +1. **Explain workflow phases**: Plan, Execute, Finish. +2. **Define skill routing**: which skill or agent the AI should use when the user expresses a certain intent. +3. **Provide workflow-state prompt blocks**: hooks can inject the prompt block for the current state into the conversation. + +## Current Phase Model + +```text +Phase 1: Plan -> clarify what to build, produce prd.md and required research +Phase 2: Execute -> implement against the PRD and specs, then check +Phase 3: Finish -> final verification, preserve lessons, and wrap up +``` + +Each phase contains numbered steps, such as `1.3 Configure context`. These numbers are not runtime fields in `task.json`; they are workflow structure for AI and humans to read. + +## Skill Routing + +`workflow.md` separates routing by platform capability: + +- Platforms with sub-agent support: dispatch `trellis-implement` by default for implementation and `trellis-check` for checking. +- Platforms without sub-agent support: the main session reads skills such as `trellis-before-dev`, then executes directly. + +When changing local AI behavior, update the routing descriptions in `workflow.md` first, then check whether the corresponding platform skill, command, or agent files need to stay in sync. + +## Workflow-State Prompt Blocks + +The bottom of `workflow.md` can contain state blocks like this: + +```text +[workflow-state:no_task] +... +[/workflow-state:no_task] +``` + +Hooks choose the right block based on current task status and inject it into the conversation. Common states include: + +| State | Meaning | +| --- | --- | +| `no_task` | The current session has no active task. | +| `planning` | The task is still in requirements, research, or context configuration. | +| `in_progress` | The task has entered implementation and checking. | +| `completed` | The task is complete and waiting for wrap-up or archive. | + +If the user wants to change policies such as "whether to create a task when there is no task," "when task creation may be skipped," or "whether sub-agents are required," edit these state blocks and the routing table above them. + +## Local Modification Patterns + +Common changes: + +| Goal | Edit point | +| --- | --- | +| Add a phase | Update the Phase Index, phase body, routing, and state blocks. | +| Change task creation policy | Update the `no_task` state block and Phase 1 description. | +| Change the default implementation/check path | Update Phase 2 and skill routing. | +| Change the wrap-up flow | Update Phase 3 and `finish-work` related descriptions. Note the current split: Phase 3.4 = AI-driven code commits (batched, user-confirmed), Phase 3.5 = `/finish-work` (archive + record session). `/finish-work` refuses to run if the working tree is dirty. | +| Change platform differences | Update routing descriptions grouped by platform. | + +After editing, make the AI reread `.trellis/workflow.md`; do not assume the flow from the old conversation is still valid. + +## Relationship To Platform Files + +`workflow.md` is the semantic center of the local workflow, but each platform can also have its own entry files: + +- skills, such as `trellis-brainstorm` and `trellis-check`. +- commands/prompts/workflows, such as continue and finish-work. +- hooks, such as session-start or workflow-state injection. + +If only `workflow.md` changes, platform entry files may still contain old language. When the user wants to change "what the AI actually does," also inspect the relevant platform directory. diff --git a/.agents/skills/trellis-meta/references/local-architecture/workspace-memory.md b/.agents/skills/trellis-meta/references/local-architecture/workspace-memory.md new file mode 100644 index 0000000..c2958f2 --- /dev/null +++ b/.agents/skills/trellis-meta/references/local-architecture/workspace-memory.md @@ -0,0 +1,71 @@ +# Local Workspace Memory System + +`.trellis/workspace/` stores cross-session memory. Its purpose is to let AI and humans understand what happened before across different windows and different days. + +## Directory Structure + +```text +.trellis/workspace/ +├── index.md +└── <developer>/ + ├── index.md + ├── journal-1.md + └── journal-2.md +``` + +| File | Purpose | +| --- | --- | +| `.trellis/.developer` | Current developer identity. | +| `.trellis/workspace/index.md` | Global workspace overview. | +| `.trellis/workspace/<developer>/index.md` | Session index for a developer. | +| `.trellis/workspace/<developer>/journal-N.md` | Session journal. | + +## Developer Identity + +Run this the first time: + +```bash +python3 ./.trellis/scripts/init_developer.py <name> +``` + +This creates `.trellis/.developer` and the corresponding workspace directory. The AI should not change developer identity casually; if the identity is wrong, first confirm who is using the current project. + +## Journal + +`journal-N.md` records completed or partially completed work from each session. By default, each journal holds about 2000 lines; after that it rotates to the next file. + +Common command for recording a session: + +```bash +python3 ./.trellis/scripts/add_session.py \ + --title "Session title" \ + --summary "What changed" \ + --commit "abc1234" +``` + +Planning or review work without a commit can also be recorded by using `--no-commit` or an empty commit value. + +## Relationship Between Workspace Memory And Tasks + +| System | What it stores | +| --- | --- | +| `.trellis/tasks/` | Requirements, design, research, and state for a specific task. | +| `.trellis/workspace/` | Work records across tasks and sessions. | +| `.trellis/spec/` | Engineering knowledge preserved as long-term conventions. | + +If information is only useful for the current task, put it in the task directory. +If information describes what happened in the current session, put it in the workspace journal. +If information should be followed every time code is written in the future, put it in spec. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change maximum journal lines | `max_journal_lines` in `.trellis/config.yaml`. | +| Change session auto-commit message | `session_commit_message` in `.trellis/config.yaml`. | +| Change session content format | `.trellis/scripts/add_session.py`. | +| Change how workspace is displayed in context | `.trellis/scripts/common/session_context.py`. | + +## AI Usage Rules + +The AI should not treat workspace as the only source of truth. When resuming a task, read the current task first, then use workspace for background. After a task is complete, record important process notes in workspace; if long-term rules emerged, update spec. diff --git a/.agents/skills/trellis-meta/references/platform-files/agents.md b/.agents/skills/trellis-meta/references/platform-files/agents.md new file mode 100644 index 0000000..a07cae7 --- /dev/null +++ b/.agents/skills/trellis-meta/references/platform-files/agents.md @@ -0,0 +1,82 @@ +# Agents + +Trellis agent files define specialized roles. Common Trellis agents in a user project are: + +- `trellis-research` +- `trellis-implement` +- `trellis-check` + +File locations and formats differ by platform, but responsibility boundaries should stay consistent. + +## Agent Responsibilities + +| Agent | Responsibility | +| --- | --- | +| `trellis-research` | Investigate the question and write findings into the current task's `research/`. | +| `trellis-implement` | Implement against `prd.md`, 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. + +## Common Paths + +| Platform | Agent path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/cli/agents/trellis-*.md` | + +GitHub Copilot agent/prompt support is provided by a combination of directories such as `.github/agents/`, `.github/prompts/`, and `.github/skills/`; inspect the files actually generated in the user project. + +Main-session workflow platforms such as Kilo, Antigravity, and Devin may not have Trellis sub-agent files. They usually rely on workflows/skills to guide the main session. + +## Two Context Loading Modes + +### hook push + +The platform hook injects task context before the agent starts. The agent file itself can focus more on responsibilities and boundaries. + +Common on platforms that support agent hooks. + +### agent pull + +The agent file instructs the agent to read after startup: + +- `python3 ./.trellis/scripts/task.py current --source` +- `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. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Implement agent must follow extra restrictions | The platform's `trellis-implement` agent file. | +| Check agent must run project-specific commands | `trellis-check` agent file, and `.trellis/spec/` if needed. | +| Research agent must output a fixed format | `trellis-research` agent file. | +| Agent cannot read task context | Agent prelude or `inject-subagent-context` hook. | +| Add a project-specific agent | Platform agent directory + related workflow/command/skill entry point. | + +## Modification Principles + +1. **Keep responsibilities single-purpose**. Do not mix research, implement, and check responsibilities into one agent. +2. **Specify the read order**. Agents must know to start from the active task, 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. + +## Do Not Default To Editing Upstream Templates + +Local AI should default to modifying platform agent files inside the user project. Discuss upstream template source only when the user explicitly wants to contribute the change back to Trellis. 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 new file mode 100644 index 0000000..d4ae8f6 --- /dev/null +++ b/.agents/skills/trellis-meta/references/platform-files/hooks-and-settings.md @@ -0,0 +1,72 @@ +# Hooks And Settings + +Hooks/settings are the entry layer that connects a platform to Trellis. They decide which scripts, plugins, or extensions a platform runs for which events. + +## Settings Responsibilities + +settings/config files usually register: + +- session-start hook: injects a Trellis overview when a new session starts or context resets. +- workflow-state hook: parses `[workflow-state:STATUS]` blocks from `.trellis/workflow.md` and emits the body matching the current task `status` on each user input. Parser-only; the script does not embed fallback content. +- sub-agent context hook: injects task context when implementation/check/research agents start. +- shell/session bridge: lets shell commands see the same Trellis session identity. +- platform plugin or extension entry points. + +Common files: + +| Platform | settings/config | +| --- | --- | +| Claude Code | `.claude/settings.json` | +| Cursor | `.cursor/hooks.json` | +| Codex | `.codex/hooks.json`, `.codex/config.toml` | +| OpenCode | `.opencode/package.json`, `.opencode/plugins/*` | +| Kiro | `.kiro/hooks/` + platform config | +| Gemini CLI | `.gemini/settings.json` | +| Qoder | `.qoder/settings.json` | +| CodeBuddy | `.codebuddy/settings.json` | +| GitHub Copilot | `.github/copilot/hooks.json` | +| Factory Droid | `.factory/settings.json` | +| Pi Agent | `.pi/settings.json`, `.pi/extensions/trellis/` | +| Trae IDE | `.trae/hooks.json` | + +Reasonix and ZCode are pull-based platforms that do not use hooks or settings files; their agent files contain prelude instructions to read context after startup. + +Whether these files exist in a project depends on which `trellis init --<platform>` flags the user ran. + +## Hook Script Types + +| Script | Purpose | +| --- | --- | +| `session-start.py` | Generates session-start context. | +| `inject-workflow-state.py` | Parses `[workflow-state:STATUS]` blocks in `.trellis/workflow.md` and emits the body matching the current task status. Falls back to `Refer to workflow.md for current step.` when no matching block exists. | +| `inject-subagent-context.py` | Injects PRD, JSONL context, and related spec/research into sub-agents. | +| `inject-shell-session-context.py` | Lets shell commands inherit Trellis session identity. | + +Not every platform has every hook. Do not copy files from another platform just because a platform lacks a hook; first confirm whether that platform supports the corresponding event. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| AI should see more/less context in a new session | Platform `session-start` hook. | +| Per-turn hint policy should change | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook parses workflow.md verbatim — no script edit required. | +| Sub-agent cannot read PRD/spec | `inject-subagent-context` hook or agent prelude. | +| `task.py current` in shell has no active task | Shell/session bridge hook or platform environment variable configuration. | +| Disable an automatic injection | The corresponding hook registration in settings/config. | + +## Modification Principles + +1. **Settings wire things up; hooks define behavior**. If only the hook changes, the platform may never call it. If only settings change, behavior may not change. +2. **Confirm platform event names first**. Different platforms use different names for SessionStart, UserPromptSubmit, AgentSpawn, shell execution, and similar events. +3. **Hooks read local `.trellis/`, not upstream source**. `.trellis/scripts/` and `.trellis/workflow.md` in the user project are the default targets. +4. **Errors must be visible**. Hook failures should tell the user what was not injected instead of silently leaving the AI without context. + +## Troubleshooting Path + +If the user says "AI did not read Trellis state": + +1. Check whether the platform settings register the hook. +2. Check whether the hook file exists. +3. Manually run the `.trellis/scripts/get_context.py` or `task.py current --source` command that the hook depends on. +4. Check whether active task state exists in `.trellis/.runtime/sessions/`. +5. Check whether the platform shell passes session identity. diff --git a/.agents/skills/trellis-meta/references/platform-files/overview.md b/.agents/skills/trellis-meta/references/platform-files/overview.md new file mode 100644 index 0000000..e403858 --- /dev/null +++ b/.agents/skills/trellis-meta/references/platform-files/overview.md @@ -0,0 +1,59 @@ +# Platform Files Overview + +Trellis connects the same local architecture to different AI tools. `.trellis/` stores the shared runtime; platform directories store adapter files that define how each AI tool enters Trellis. + +When a local AI modifies Trellis, it should distinguish two file categories first: + +- **Shared files**: `.trellis/workflow.md`, `.trellis/tasks/`, `.trellis/spec/`, `.trellis/scripts/`. +- **Platform files**: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.trae/`, `.kilocode/`, `.agent/`, `.devin/`, `.reasonix/`, `.zcode/`, and similar directories. + +Platform files do not store business state. They let the corresponding AI tool read Trellis state, call Trellis scripts, and load Trellis skills/agents/hooks. + +## Platform File Categories + +| Category | Common paths | Purpose | +| --- | --- | --- | +| settings/config | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.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/`, `.devin/workflows/` | Entry points explicitly invoked by the user. | + +## Three Platform Integration Modes + +### 1. Hook / Extension Driven + +These platforms can trigger scripts or plugins on specific events and actively inject Trellis context into AI. + +Common capabilities: + +- session-start injection of a `.trellis/` overview. +- workflow-state hints for each user turn. +- PRD/spec/research injection when sub-agents start. +- Shell commands inheriting session identity. + +To change "when the AI knows what," inspect hooks/plugins/extensions and settings first. + +### 2. Agent Prelude / Pull-Based + +Some platforms cannot reliably let hooks rewrite sub-agent prompts, so the agent file itself instructs the agent to read the active task, PRD, and JSONL context after startup. + +To change how sub-agents load context, inspect the agent files themselves. + +### 3. Main-Session Workflow + +Some platforms do not have Trellis sub-agent or hook capabilities. They rely on workflows/skills/commands to guide the main-session AI to read files, run scripts, and move tasks forward. + +To change behavior, inspect platform workflows/skills/commands and `.trellis/workflow.md`. + +## Local Modification Order + +When the user asks to customize behavior for a platform, the AI should inspect files in this order: + +1. Read `.trellis/workflow.md` to confirm the shared flow. +2. Read the target platform's settings/config to see which hooks/agents/skills/commands are registered. +3. Read the target platform's agents/skills/commands/hooks. +4. Modify the local file closest to the user's need. +5. If the change affects the shared flow, synchronize `.trellis/workflow.md` or `.trellis/spec/`. + +Do not modify only platform files and forget the shared workflow. Do not modify only `.trellis/workflow.md` and forget that platform entry points may still contain old descriptions. diff --git a/.agents/skills/trellis-meta/references/platform-files/platform-map.md b/.agents/skills/trellis-meta/references/platform-files/platform-map.md new file mode 100644 index 0000000..dddb743 --- /dev/null +++ b/.agents/skills/trellis-meta/references/platform-files/platform-map.md @@ -0,0 +1,88 @@ +# Platform File Map + +This page lists common Trellis file locations in a user project by platform. Whether a platform directory exists in an actual project depends on which `trellis init --<platform>` commands the user ran. + +## Matrix + +| Platform | CLI flag | Main directory | Skill directory | Agent directory | Hooks/extensions | +| --- | --- | --- | --- | --- | --- | +| Claude Code | `--claude` | `.claude/` | `.claude/skills/` | `.claude/agents/` | `.claude/hooks/` + `.claude/settings.json` | +| Cursor | `--cursor` | `.cursor/` | `.cursor/skills/` | `.cursor/agents/` | `.cursor/hooks.json` + `.cursor/hooks/` | +| OpenCode | `--opencode` | `.opencode/` | `.opencode/skills/` | `.opencode/agents/` | `.opencode/plugins/` | +| Codex | `--codex` | `.codex/` | `.agents/skills/` | `.codex/agents/` | `.codex/hooks/` + `.codex/hooks.json` | +| Kilo | `--kilo` | `.kilocode/` | `.kilocode/skills/` | Usually none | `.kilocode/workflows/` | +| Kiro | `--kiro` | `.kiro/` | `.kiro/skills/` | `.kiro/agents/` | `.kiro/hooks/` | +| Gemini CLI | `--gemini` | `.gemini/` | `.agents/skills/` | `.gemini/agents/` | `.gemini/settings.json` + `.gemini/hooks/` | +| Antigravity | `--antigravity` | `.agent/` | `.agent/skills/` | Usually none | `.agent/workflows/` | +| 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/` (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/` | `.agents/skills/` | `.zcode/cli/agents/` | pull-based prelude (no hooks) | + +## Capability Groups + +### Trellis Sub-Agent Support + +These platforms usually have `trellis-research`, `trellis-implement`, and `trellis-check` files: + +- Claude Code +- Cursor +- OpenCode +- Codex +- Kiro +- Gemini CLI +- Qoder +- CodeBuddy +- GitHub Copilot +- Factory Droid +- Pi Agent +- Trae IDE +- Reasonix (delivered as skills with `runAs: subagent` under `.reasonix/skills/`, not as a separate `agents/` directory) +- ZCode + +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 +- 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. + +## Decision Rules When Modifying Platform Files + +1. User specified a platform: modify only that platform directory unless shared workflow/spec files must also change. +2. User says "all platforms should do this": synchronize equivalent entry points platform by platform; do not modify only one directory. +3. User only says "my AI": inspect the configuration directories that actually exist in the project and infer the current AI platform. +4. User wants project rules: prefer `.trellis/spec/` or a project-local skill. +5. User wants Trellis behavior: edit `.trellis/workflow.md` plus platform hooks/agents/skills/commands. + +## When Paths Differ + +Platform ecosystems change, and user projects may already be customized. If this table disagrees with local files, use the actual settings/config in the user project as authoritative: + +- Check the hook that settings registers. +- Check the script that a command/prompt/workflow points to. +- Judge behavior by the read rules currently written in the agent file. + +Do not delete a custom file just because it is not listed in this path table. 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 new file mode 100644 index 0000000..4732c6a --- /dev/null +++ b/.agents/skills/trellis-meta/references/platform-files/skills-and-commands.md @@ -0,0 +1,85 @@ +# Skills, Commands, Prompts, And Workflows + +Skills and commands are textual entry points for user interaction with Trellis. Different platforms use different names, but their core purpose is the same: tell the AI how to enter the Trellis flow when the user expresses a certain intent. + +## Conceptual Differences + +| Type | Trigger mode | Best for | +| --- | --- | --- | +| skill | AI auto-match or explicit user mention | Long-term capabilities, workflow rules, modification guides. | +| command | Explicit user invocation | Clear operation entry points such as continue and finish-work. | +| prompt | Explicit user invocation or platform selection | Similar to command, but in a platform prompt format. | +| workflow | Explicit user selection or platform auto-match | Guides the main session when no sub-agent/hook exists. | + +Trellis workflow skills usually share one semantic set: brainstorm, before-dev, check, update-spec, break-loop. Multi-file built-in skills such as `trellis-meta` use layered references. + +## Common Paths + +| Platform | Common entries | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| Kilo | `.kilocode/skills/`, `.kilocode/workflows/` | +| Kiro | `.kiro/skills/` | +| Gemini CLI | `.agents/skills/`, `.gemini/commands/` | +| Antigravity | `.agent/skills/`, `.agent/workflows/` | +| 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/` | +| Reasonix | `.reasonix/skills/` | +| ZCode | `.agents/skills/`, `.zcode/commands/` | + +In a user project, use the files actually generated by init as authoritative. + +## Skill Structure + +A common skill is a directory: + +```text +trellis-meta/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should tell the AI: + +- When to use this skill. +- Which reference to read first for the current task. +- What not to do. + +References hold longer explanations so the entry file does not contain everything. + +## Command/Prompt/Workflow Structure + +Commands, prompts, and workflows are usually single files. Their content should include: + +- When to use it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +They should not store task state; task state belongs in `.trellis/tasks/` and `.trellis/.runtime/`. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Change AI auto-trigger rules | The corresponding skill's frontmatter description. | +| Change user command behavior | The corresponding command/prompt/workflow file. | +| Add a project-local skill | Platform skill directory, or shared `.agents/skills/`. | +| Let multiple platforms share one capability | Write equivalent skills in each platform skill directory, or use the `.agents/skills/` shared layer on platforms that support it. | +| Change finish/continue entry points | Platform commands/prompts/workflows. | + +## Modification Principles + +1. **Keep entry files short; references carry long content**. This matters especially for multi-file skills like `trellis-meta`. +2. **Make trigger descriptions specific**. A description that is too broad can mis-trigger; one that is too narrow may not trigger. +3. **Keep the same semantics consistent across platforms**. File formats can differ, but behavior descriptions should match. +4. **Put project-specific capabilities in local skills**. Do not put team-private flows into public `trellis-meta`. + +If the user only wants local AI to know one more project rule, usually create a project-local skill or update `.trellis/spec/` instead of changing a Trellis built-in workflow skill. diff --git a/.agents/skills/trellis-session-insight/SKILL.md b/.agents/skills/trellis-session-insight/SKILL.md new file mode 100644 index 0000000..8e2cbb7 --- /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, and Pi Agent conversation logs (the JSONL files each platform stores under `~/.claude/projects/`, `~/.codex/sessions/`, and `~/.pi/agent/sessions/`) and lets you list, search, slice by Trellis task boundaries, and dump cleaned dialogue from them. 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 0000000..78540f2 --- /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 0000000..66021ca --- /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-bootstrap/SKILL.md b/.agents/skills/trellis-spec-bootstrap/SKILL.md new file mode 100644 index 0000000..e1650df --- /dev/null +++ b/.agents/skills/trellis-spec-bootstrap/SKILL.md @@ -0,0 +1,41 @@ +--- +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 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. + +## Workflow + +1. Confirm Trellis is initialized and inspect the current `.trellis/spec/` tree. +2. Analyze the repository architecture with the best available tools: GitNexus, ABCoder, language tooling, and direct source reads. +3. Decompose the spec work by package and layer only when that reflects the actual codebase. +4. Fill or reshape the spec files with concrete patterns, file paths, examples, and anti-patterns from the project. +5. Verify that the final specs are internally consistent and contain no template placeholders. + +## Reference Routing + +| Need | Read | +|------|------| +| Repository architecture analysis | [references/repository-analysis.md](references/repository-analysis.md) | +| Spec work decomposition and task planning | [references/spec-task-planning.md](references/spec-task-planning.md) | +| Writing high-signal Trellis spec files | [references/spec-writing.md](references/spec-writing.md) | +| GitNexus and ABCoder MCP setup | [references/mcp-setup.md](references/mcp-setup.md) | + +## Operating Rules + +- Treat templates as starting points, not contracts. Delete, rename, split, or add spec files when the repository calls for it. +- Prefer source-backed rules over generic advice. Every important recommendation should point at a real file or repeated local pattern. +- Keep execution single-owner by default. Optional helper agents are an implementation detail, not a requirement or user-visible dependency. +- Do not write platform-specific instructions unless the target project already standardizes on that platform. +- Do not leave placeholder text, empty headings, or copied boilerplate in `.trellis/spec/`. + +## Done Criteria + +- `.trellis/spec/` describes the project as it exists now. +- Each relevant package or layer has practical coding guidance with real examples. +- Non-applicable template sections are removed. +- `index.md` files match the final spec file set. +- Any required setup or analysis assumptions are documented in the relevant spec or task notes. diff --git a/.agents/skills/trellis-spec-bootstrap/references/mcp-setup.md b/.agents/skills/trellis-spec-bootstrap/references/mcp-setup.md new file mode 100644 index 0000000..629fcbd --- /dev/null +++ b/.agents/skills/trellis-spec-bootstrap/references/mcp-setup.md @@ -0,0 +1,90 @@ +# MCP Setup + +GitNexus and ABCoder are recommended when bootstrapping Trellis specs because they expose architecture and AST context to the agent. They are tool choices, not platform requirements. Configure them through whatever MCP mechanism your agent host provides. + +## GitNexus + +GitNexus builds a code knowledge graph from the repository. Use it for module boundaries, execution flows, dependency relationships, blast radius, and graph queries. + +### Install and Index + +```bash +# Run from the repository root. +npx gitnexus analyze + +# Check index status. +npx gitnexus status + +# Re-index after code changes when the analysis is stale. +npx gitnexus analyze +``` + +The index is written to `.gitnexus/`. Keep embeddings only if the project already uses them; otherwise a normal index is enough for spec bootstrapping. + +### MCP Server Command + +Use this server command in the host's MCP configuration: + +```bash +npx -y gitnexus mcp +``` + +### Useful Tools + +| Tool | Purpose | +|------|---------| +| `gitnexus_query` | Find execution flows and functional areas by concept | +| `gitnexus_context` | Inspect callers, callees, references, and process participation for a symbol | +| `gitnexus_impact` | Understand blast radius before changing a symbol | +| `gitnexus_detect_changes` | Check changed symbols and affected flows before finishing | +| `gitnexus_cypher` | Run direct graph queries | +| `gitnexus_list_repos` | List indexed repositories | + +## ABCoder + +ABCoder parses code into UniAST and gives precise package, file, and node-level structure. Use it for signatures, type shapes, implementations, dependencies, and reverse references. + +### Install + +```bash +go install github.com/cloudwego/abcoder@latest +abcoder --help +``` + +### Parse Repositories + +```bash +abcoder parse /absolute/path/to/package \ + --lang typescript \ + --name package-name \ + --output ~/abcoder-asts +``` + +For monorepos, parse each package with a stable `--name` so task notes can reference the same repository names. + +### MCP Server Command + +Use this server command in the host's MCP configuration: + +```bash +abcoder mcp ~/abcoder-asts +``` + +### Useful Tools + +| Tool | Layer | Purpose | +|------|-------|---------| +| `list_repos` | 1 | List parsed repositories | +| `get_repo_structure` | 2 | Inspect packages and files | +| `get_package_structure` | 3 | Inspect nodes within a package | +| `get_file_structure` | 3 | Inspect functions, classes, types, and signatures in a file | +| `get_ast_node` | 4 | Retrieve code, dependencies, references, and implementations | + +## Verification + +After configuration, verify from the agent host that both MCP servers are visible. Then run one simple query against each server before starting the spec writing pass. + +```bash +ls .gitnexus/meta.json +ls ~/abcoder-asts/*.json +``` diff --git a/.agents/skills/trellis-spec-bootstrap/references/repository-analysis.md b/.agents/skills/trellis-spec-bootstrap/references/repository-analysis.md new file mode 100644 index 0000000..1309d29 --- /dev/null +++ b/.agents/skills/trellis-spec-bootstrap/references/repository-analysis.md @@ -0,0 +1,59 @@ +# Repository Analysis + +The goal is to discover the project's real architecture before writing rules. Do not start from generic spec templates and fill blanks. Start from the code, then let the spec structure follow. + +## Analysis Order + +1. Read the existing `.trellis/spec/` tree and note which files are templates, outdated, or already project-specific. +2. Inspect package manifests, build scripts, workspace config, and top-level documentation to identify packages and runtime layers. +3. Use GitNexus for execution flows, module clusters, dependency hubs, and impact-sensitive areas. +4. Use ABCoder or language-native tooling for exact signatures, types, class boundaries, and implementation examples. +5. Read representative source and test files directly before turning any finding into a spec rule. + +## What To Capture + +| Area | Questions | +|------|-----------| +| Package boundaries | What does each package own? What imports cross boundaries? | +| Runtime layers | Which code is CLI, backend, frontend, worker, shared library, test-only, or tooling? | +| Core abstractions | Which types, services, stores, commands, routes, or adapters define the system shape? | +| Data flow | Where does user input enter, how is it validated, and where does state persist? | +| Error handling | How are failures represented, logged, surfaced, and tested? | +| Configuration | Where do defaults, environment config, generated files, and templates live? | +| Tests | Which test styles are trusted examples for new work? | + +## GitNexus Usage + +Start broad, then inspect specific symbols: + +```text +gitnexus_query({query: "CLI command execution flow"}) +gitnexus_query({query: "template generation and migration"}) +gitnexus_context({name: "SymbolName"}) +gitnexus_cypher({query: "MATCH (n)-[r]->(m) RETURN n.name, type(r), m.name LIMIT 30"}) +``` + +Use GitNexus results to find important files and flows. Do not quote graph output as the final authority until you have checked the relevant source files. + +## ABCoder Usage + +Use ABCoder when the spec needs exact code shapes: + +```text +list_repos() +get_repo_structure({repo_name: "package-name"}) +get_file_structure({repo_name: "package-name", file_path: "src/example.ts"}) +get_ast_node({repo_name: "package-name", node_ids: [{mod_path: "...", pkg_path: "...", name: "SymbolName"}]}) +``` + +ABCoder is most valuable for documenting constructor patterns, function signatures, type contracts, and reference chains. + +## Analysis Notes + +Keep short notes while analyzing. The notes should include: + +- Package or layer name. +- Files that define the local pattern. +- Rules the spec should teach. +- Anti-patterns found in old code, comments, tests, or migration paths. +- Spec files that should be created, deleted, renamed, or merged. diff --git a/.agents/skills/trellis-spec-bootstrap/references/spec-task-planning.md b/.agents/skills/trellis-spec-bootstrap/references/spec-task-planning.md new file mode 100644 index 0000000..dca2687 --- /dev/null +++ b/.agents/skills/trellis-spec-bootstrap/references/spec-task-planning.md @@ -0,0 +1,61 @@ +# Spec Task Planning + +Use a single agent as the default execution model. The agent may create Trellis tasks for traceability, but the skill should not require a specific platform, CLI, or parallel worker model. + +## Decomposition + +Create spec work units around real ownership boundaries: + +- One package when a package has its own conventions. +- One layer when the same package has distinct frontend, backend, CLI, worker, or shared-library rules. +- One cross-cutting guide when a pattern spans packages and is not owned by one layer. + +Avoid artificial decomposition. A small library usually needs one focused spec pass, not several tasks. + +## Task Shape + +When a Trellis task is useful, write a concise PRD with these sections: + +```markdown +# Fill <package-or-layer> Trellis Specs + +## Goal +Write project-specific `.trellis/spec/` guidance for <scope>. + +## Scope +- Spec directory: +- Source directories to inspect: +- Tests to inspect: +- Out of scope: + +## Architecture Context +Summarize the concrete findings from repository analysis. + +## Files To Create Or Update +- `.trellis/spec/.../index.md` +- `.trellis/spec/.../<topic>.md` + +## Rules +- Adapt the spec file set to the real codebase. +- Use real source examples with file paths. +- Remove template-only sections that do not apply. +- Do not modify product source code unless the task explicitly asks for it. + +## Acceptance Criteria +- [ ] Specs contain concrete examples and anti-patterns from the repository. +- [ ] No placeholder text remains. +- [ ] Index files match the final spec files. +- [ ] Claims are backed by source files, tests, or project docs. +``` + +## Optional Helper Agents + +If the host supports subagents, helpers can inspect independent packages or run verification. They are optional. The main agent still owns integration and final quality. + +Helper tasks must have clear ownership: + +- Read-only research tasks may inspect any source needed for the assigned scope. +- Write tasks should own disjoint spec directories. +- Verification tasks should check placeholder removal, broken links, and consistency. + +Do not encode helper-agent names, vendor-specific commands, or platform-specific routing in the skill. Put only the required work and acceptance criteria in the task. diff --git a/.agents/skills/trellis-spec-bootstrap/references/spec-writing.md b/.agents/skills/trellis-spec-bootstrap/references/spec-writing.md new file mode 100644 index 0000000..6bc7dec --- /dev/null +++ b/.agents/skills/trellis-spec-bootstrap/references/spec-writing.md @@ -0,0 +1,70 @@ +# Spec Writing + +Trellis specs are coding guidance for future agents. They should explain how to work in this repository, not how a generic project might be organized. + +## Write From Evidence + +Each important rule should be backed by one of these: + +- A source file that demonstrates the preferred pattern. +- A test file that shows expected behavior. +- A project document that defines the convention. +- A repeated pattern across multiple files. + +Use short snippets only when they make the rule clearer. Prefer linking to the file path and naming the symbol or behavior. + +## File Structure + +Keep the spec tree aligned with the project: + +- Keep `index.md` as the navigation file for the spec directory. +- Split topics when developers would look for them independently. +- Merge topics when separate files would repeat the same rule. +- Delete template files that do not apply. +- Add new files for important local patterns the template missed. + +## Content Standards + +Good spec sections include: + +- When the rule applies. +- The local pattern to follow. +- The source or test files that prove the pattern. +- Common mistakes or anti-patterns. +- Verification commands or checks when they are specific and reliable. + +Avoid: + +- Placeholder prose. +- Generic framework advice. +- Tool instructions that only work in one agent host. +- Long copied code blocks. +- Rules based on a single accidental implementation detail. + +## Example Shape + +```markdown +## Command Handlers + +Command handlers should keep argument parsing, validation, and side effects separate. The local pattern is: + +- Parse CLI flags at the command boundary. +- Convert raw inputs into typed task options before invoking core logic. +- Keep filesystem writes in the command or service layer, not in template helpers. + +Reference files: +- `packages/cli/src/commands/example.ts` +- `packages/cli/test/commands/example.test.ts` + +Avoid passing raw `process.argv` or unvalidated config objects into shared helpers. +``` + +## Final Pass + +Before finishing: + +```bash +grep -R "To be filled\\|TODO: fill\\|placeholder" .trellis/spec +``` + +Also check links, index files, and whether any spec still describes a template rather than this repository. diff --git a/.agents/skills/trellis-start/SKILL.md b/.agents/skills/trellis-start/SKILL.md new file mode 100644 index 0000000..3c7980d --- /dev/null +++ b/.agents/skills/trellis-start/SKILL.md @@ -0,0 +1,64 @@ +--- +name: trellis-start +description: "Initializes an AI development session by reading workflow guides, developer identity, git status, active tasks, and project guidelines from .trellis/. Classifies incoming tasks and routes to brainstorm, direct edit, or task workflow. Use when beginning a new coding session, resuming work, starting a new task, or re-establishing project context." +--- + +# Start Session + +Initialize a Trellis-managed development session. This platform has no session-start hook, so manually load the equivalent compact context by following these steps. + +--- + +## Step 1: Current state +Identity, git status, current task, active tasks, journal location. + +```bash +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 +Compact Phase Index, request triage rules, planning artifact contract, and the step-detail command. + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase +``` + +Full guide in `.trellis/workflow.md` (read on demand). + +## Step 3: Guideline indexes +Discover packages + spec layers, then read each relevant index file. + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +cat .trellis/spec/guides/index.md +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 and status. Check the task directory: + +- **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 + ``` +- **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. + +--- + +## Skill routing (quick reference) + +| User intent | Skill | +|---|---| +| New feature / unclear requirements | `trellis-brainstorm` | +| About to write code | `trellis-before-dev` | +| Done coding / quality check | `trellis-check` | +| Stuck / fixed same bug multiple times | `trellis-break-loop` | +| Learned something worth capturing | `trellis-update-spec` | + +Full rules + anti-rationalization table in `.trellis/workflow.md`. diff --git a/.agents/skills/trellis-update-spec/SKILL.md b/.agents/skills/trellis-update-spec/SKILL.md new file mode 100644 index 0000000..81bad08 --- /dev/null +++ b/.agents/skills/trellis-update-spec/SKILL.md @@ -0,0 +1,356 @@ +--- +name: trellis-update-spec +description: "Captures executable contracts and coding conventions into .trellis/spec/ documents. Use when learning something valuable from debugging, implementing, or discussion that should be preserved for future sessions." +--- + +# Update Code-Spec - Capture Executable Contracts + +When you learn something valuable (from debugging, implementing, or discussion), use this to update the relevant code-spec documents. + +**Timing**: After completing a task, fixing a bug, or discovering a new pattern + +--- + +## Code-Spec First Rule (CRITICAL) + +In this project, "spec" for implementation work means **code-spec**: +- Executable contracts (not principle-only text) +- Concrete signatures, payload fields, env keys, and boundary behavior +- Testable validation/error behavior + +If the change touches infra or cross-layer contracts, code-spec depth is mandatory. + +### Mandatory Triggers + +Apply code-spec depth when the change includes any of: +- New/changed command or API signature +- Cross-layer request/response contract change +- Database schema/migration change +- Infra integration (storage, queue, cache, secrets, env wiring) + +### Mandatory Output (7 Sections) + +For triggered tasks, include all sections below: +1. Scope / Trigger +2. Signatures (command/API/DB) +3. Contracts (request/response/env) +4. Validation & Error Matrix +5. Good/Base/Bad Cases +6. Tests Required (with assertion points) +7. Wrong vs Correct (at least one pair) + +--- + +## When to Update Code-Specs + +| Trigger | Example | Target Spec | +|---------|---------|-------------| +| **Implemented a feature** | Added a new integration or module | Relevant spec file | +| **Made a design decision** | Chose extensibility pattern over simplicity | Relevant spec + "Design Decisions" section | +| **Fixed a bug** | Found a subtle issue with error handling | Relevant spec (e.g., error-handling docs) | +| **Discovered a pattern** | Found a better way to structure code | Relevant spec file | +| **Hit a gotcha** | Learned that X must be done before Y | Relevant spec + "Common Mistakes" section | +| **Established a convention** | Team agreed on naming pattern | Quality guidelines | +| **New thinking trigger** | "Don't forget to check X before doing Y" | `guides/*.md` (as a checklist item) | + +**Key Insight**: Code-spec updates are NOT just for problems. Every feature implementation contains design decisions and contracts that future AI/developers need to execute safely. + +--- + +## Spec Structure Overview + +``` +.trellis/spec/ +├── <layer>/ # Per-layer coding standards (e.g., backend/, frontend/, api/) +│ ├── index.md # Overview and links +│ └── *.md # Topic-specific guidelines +└── guides/ # Thinking checklists (NOT coding specs!) + ├── index.md # Guide index + └── *.md # Topic-specific guides +``` + +### CRITICAL: Code-Spec vs Guide - Know the Difference + +| Type | Location | Purpose | Content Style | +|------|----------|---------|---------------| +| **Code-Spec** | `<layer>/*.md` | Tell AI "how to implement safely" | Signatures, contracts, matrices, cases, test points | +| **Guide** | `guides/*.md` | Help AI "what to think about" | Checklists, questions, pointers to specs | + +**Decision Rule**: Ask yourself: + +- "This is **how to write** the code" → Put in a spec layer directory +- "This is **what to consider** before writing" → Put in `guides/` + +**Example**: + +| Learning | Wrong Location | Correct Location | +|----------|----------------|------------------| +| "Use API X not API Y for this task" | ❌ `guides/` (too specific for a thinking guide) | ✅ Relevant spec file (concrete convention) | +| "Remember to check X when doing Y" | ❌ Spec file (too abstract for a spec) | ✅ `guides/` (thinking checklist) | + +**Guides should be short checklists that point to specs**, not duplicate the detailed rules. + +--- + +## Update Process + +### Step 1: Identify What You Learned + +Answer these questions: + +1. **What did you learn?** (Be specific) +2. **Why is it important?** (What problem does it prevent?) +3. **Where does it belong?** (Which spec file?) + +### Step 2: Classify the Update Type + +| Type | Description | Action | +|------|-------------|--------| +| **Design Decision** | Why we chose approach X over Y | Add to "Design Decisions" section | +| **Project Convention** | How we do X in this project | Add to relevant section with examples | +| **New Pattern** | A reusable approach discovered | Add to "Patterns" section | +| **Forbidden Pattern** | Something that causes problems | Add to "Anti-patterns" or "Don't" section | +| **Common Mistake** | Easy-to-make error | Add to "Common Mistakes" section | +| **Convention** | Agreed-upon standard | Add to relevant section | +| **Gotcha** | Non-obvious behavior | Add warning callout | + +### Step 3: Read the Target Code-Spec + +Before editing, read the current code-spec to: +- Understand existing structure +- Avoid duplicating content +- Find the right section for your update + +```bash +cat .trellis/spec/<category>/<file>.md +``` + +### Step 4: Make the Update + +Follow these principles: + +1. **Be Specific**: Include concrete examples, not just abstract rules +2. **Explain Why**: State the problem this prevents +3. **Show Contracts**: Add signatures, payload fields, and error behavior +4. **Show Code**: Add code snippets for key patterns +5. **Keep it Short**: One concept per section + +### Step 5: Update the Index (if needed) + +If you added a new section or the code-spec status changed, update the category's `index.md`. + +--- + +## Update Templates + +### Mandatory Template for Infra/Cross-Layer Work + +```markdown +## Scenario: <name> + +### 1. Scope / Trigger +- Trigger: <why this requires code-spec depth> + +### 2. Signatures +- Backend command/API/DB signature(s) + +### 3. Contracts +- Request fields (name, type, constraints) +- Response fields (name, type, constraints) +- Environment keys (required/optional) + +### 4. Validation & Error Matrix +- <condition> -> <error> + +### 5. Good/Base/Bad Cases +- Good: ... +- Base: ... +- Bad: ... + +### 6. Tests Required +- Unit/Integration/E2E with assertion points + +### 7. Wrong vs Correct +#### Wrong +... +#### Correct +... +``` + +### Adding a Design Decision + +```markdown +### Design Decision: [Decision Name] + +**Context**: What problem were we solving? + +**Options Considered**: +1. Option A - brief description +2. Option B - brief description + +**Decision**: We chose Option X because... + +**Example**: +\`\`\`typescript +// How it's implemented +code example +\`\`\` + +**Extensibility**: How to extend this in the future... +``` + +### Adding a Project Convention + +```markdown +### Convention: [Convention Name] + +**What**: Brief description of the convention. + +**Why**: Why we do it this way in this project. + +**Example**: +\`\`\`typescript +// How to follow this convention +code example +\`\`\` + +**Related**: Links to related conventions or specs. +``` + +### Adding a New Pattern + +```markdown +### Pattern Name + +**Problem**: What problem does this solve? + +**Solution**: Brief description of the approach. + +**Example**: +\`\`\` +// Good +code example + +// Bad +code example +\`\`\` + +**Why**: Explanation of why this works better. +``` + +### Adding a Forbidden Pattern + +```markdown +### Don't: Pattern Name + +**Problem**: +\`\`\` +// Don't do this +bad code example +\`\`\` + +**Why it's bad**: Explanation of the issue. + +**Instead**: +\`\`\` +// Do this instead +good code example +\`\`\` +``` + +### Adding a Common Mistake + +```markdown +### Common Mistake: Description + +**Symptom**: What goes wrong + +**Cause**: Why this happens + +**Fix**: How to correct it + +**Prevention**: How to avoid it in the future +``` + +### Adding a Gotcha + +```markdown +> **Warning**: Brief description of the non-obvious behavior. +> +> Details about when this happens and how to handle it. +``` + +--- + +## Interactive Mode + +If you're unsure what to update, answer these prompts: + +1. **What did you just finish?** + - [ ] Fixed a bug + - [ ] Implemented a feature + - [ ] Refactored code + - [ ] Had a discussion about approach + +2. **What did you learn or decide?** + - Design decision (why X over Y) + - Project convention (how we do X) + - Non-obvious behavior (gotcha) + - Better approach (pattern) + +3. **Would future AI/developers need to know this?** + - To understand how the code works → Yes, update spec + - To maintain or extend the feature → Yes, update spec + - To avoid repeating mistakes → Yes, update spec + - Purely one-off implementation detail → Maybe skip + +4. **Which area does it relate to?** + - [ ] Backend code + - [ ] Frontend code + - [ ] Cross-layer data flow + - [ ] Code organization/reuse + - [ ] Quality/testing + +--- + +## Quality Checklist + +Before finishing your code-spec update: + +- [ ] Is the content specific and actionable? +- [ ] Did you include a code example? +- [ ] Did you explain WHY, not just WHAT? +- [ ] Did you include executable signatures/contracts? +- [ ] Did you include validation and error matrix? +- [ ] Did you include Good/Base/Bad cases? +- [ ] Did you include required tests with assertion points? +- [ ] Is it in the right code-spec file? +- [ ] Does it duplicate existing content? +- [ ] Would a new team member understand it? + +--- + +## Relationship to Other Commands + +``` +Development Flow: + Learn something → `update-spec` (Trellis command) → Knowledge captured + ↑ ↓ + `break-loop` (Trellis command) ←──────────────────── Future sessions benefit + (deep bug analysis) +``` + +- ``break-loop` (Trellis command)` - Analyzes bugs deeply, often reveals spec updates needed +- ``update-spec` (Trellis command)` - Actually makes the updates +- ``finish-work` (Trellis command)` - Reminds you to check if specs need updates + +--- + +## Core Philosophy + +> **Code-specs are living documents. Every debugging session, every "aha moment" is an opportunity to make the implementation contract clearer.** + +The goal is **institutional memory**: +- What one person learns, everyone benefits from +- What AI learns in one session, persists to future sessions +- Mistakes become documented guardrails diff --git a/.claude/agents/trellis-check.md b/.claude/agents/trellis-check.md new file mode 100644 index 0000000..14334f2 --- /dev/null +++ b/.claude/agents/trellis-check.md @@ -0,0 +1,115 @@ +--- +name: trellis-check +description: | + Code quality check expert. Reviews code changes against specs and self-fixes issues. +tools: Read, Write, Edit, Bash, Glob, Grep +--- +# Check Agent + +You are the Check Agent in the Trellis workflow. + +## Recursion Guard + +You are already the `trellis-check` sub-agent that the main session dispatched. Do the review and fixes directly. + +- Do NOT spawn another `trellis-check` or `trellis-implement` sub-agent. +- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. +- Only the main session may dispatch Trellis implement/check agents. If more implementation work is needed, report that recommendation instead of spawning. + +## Trellis Context Loading Protocol + +Look for the `<!-- trellis-hook-injected -->` marker in your input above. + +- **If the marker is present**: task artifacts, spec, and research files have already been auto-loaded for you above. Proceed with the check work directly. +- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: <path>`, then Read `<task-path>/check.jsonl`, each listed file, `<task-path>/prd.md`, `<task-path>/design.md` if present, and `<task-path>/implement.md` if present before doing the work. + +## Context + +Before checking, read: +- `.trellis/spec/` - Development guidelines +- Task `prd.md` - Requirements document +- Task `design.md` - Technical design (if exists) +- Task `implement.md` - Execution plan (if exists) +- Pre-commit checklist for quality standards + +## Core Responsibilities + +1. **Get code changes** - Use git diff to get uncommitted code +2. **Review task artifacts** - Check changes against prd.md, design.md if present, and implement.md if present +3. **Check against specs** - Verify code follows guidelines +4. **Self-fix** - Fix issues yourself, not just report them +5. **Run verification** - typecheck and lint + +## Important + +**Fix issues yourself**, don't just report them. + +You have write and edit tools, you can modify code directly. + +--- + +## Workflow + +### Step 1: Get Changes + +```bash +git diff --name-only # List changed files +git diff # View specific changes +``` + +### Step 2: Check Against Specs and Task Artifacts + +Read the task's prd.md, design.md if present, and implement.md if present, then read relevant specs in `.trellis/spec/` to check code: + +- Does it satisfy the task requirements +- Does it follow the technical design and implementation plan when present +- Does it follow directory structure conventions +- Does it follow naming conventions +- Does it follow code patterns +- Are there missing types +- Are there potential bugs + +### Step 3: Self-Fix + +After finding issues: + +1. Fix the issue directly (use edit tool) +2. Record what was fixed +3. Continue checking other issues + +### Step 4: Run Verification + +Run project's lint and typecheck commands to verify changes. + +If failed, fix issues and re-run. + +--- + +## Report Format + +```markdown +## Self-Check Complete + +### Files Checked + +- src/components/Feature.tsx +- src/hooks/useFeature.ts + +### Issues Found and Fixed + +1. `<file>:<line>` - <what was fixed> +2. `<file>:<line>` - <what was fixed> + +### Issues Not Fixed + +(If there are issues that cannot be self-fixed, list them here with reasons) + +### Verification Results + +- TypeCheck: Passed +- Lint: Passed + +### Summary + +Checked X files, found Y issues, all fixed. +``` diff --git a/.claude/agents/trellis-implement.md b/.claude/agents/trellis-implement.md new file mode 100644 index 0000000..333bf73 --- /dev/null +++ b/.claude/agents/trellis-implement.md @@ -0,0 +1,110 @@ +--- +name: trellis-implement +description: | + Code implementation expert. Understands specs and requirements, then implements features. No git commit allowed. +tools: Read, Write, Edit, Bash, Glob, Grep +--- +# Implement Agent + +You are the Implement Agent in the Trellis workflow. + +## Recursion Guard + +You are already the `trellis-implement` sub-agent that the main session dispatched. Do the implementation work directly. + +- Do NOT spawn another `trellis-implement` or `trellis-check` sub-agent. +- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. +- Only the main session may dispatch Trellis implement/check agents. If more parallel work is needed, report that recommendation instead of spawning. + +## Trellis Context Loading Protocol + +Look for the `<!-- trellis-hook-injected -->` marker in your input above. + +- **If the marker is present**: prd / spec / research files have already been auto-loaded for you above. Proceed with the implementation work directly. +- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: <path>`, then Read `<task-path>/implement.jsonl`, each listed file, `<task-path>/prd.md`, `<task-path>/design.md` if present, and `<task-path>/implement.md` if present before doing the work. + +## Context + +Before implementing, read: +- `.trellis/workflow.md` - Project workflow +- `.trellis/spec/` - Development guidelines +- Task `prd.md` - Requirements document +- Task `design.md` - Technical design (if exists) +- Task `implement.md` - Execution plan (if exists) + +## Core Responsibilities + +1. **Understand specs** - Read relevant spec files in `.trellis/spec/` +2. **Understand task artifacts** - Read prd.md, design.md if present, and implement.md if present +3. **Implement features** - Write code following specs and task artifacts +4. **Self-check** - Ensure code quality +5. **Report results** - Report completion status + +## Forbidden Operations + +**Do NOT execute these git commands:** + +- `git commit` +- `git push` +- `git merge` + +--- + +## Workflow + +### 1. Understand Specs + +Read relevant specs based on task type: + +- Spec layers: `.trellis/spec/<package>/<layer>/` +- Shared guides: `.trellis/spec/guides/` + +### 2. Understand Requirements + +Read the task's prd.md, design.md if present, and implement.md if present: + +- What are the core requirements +- Key points of technical design +- Implementation order, validation commands, and rollback points + +### 3. Implement Features + +- Write code following specs and task artifacts +- Follow existing code patterns +- Only do what's required, no over-engineering + +### 4. Verify + +Run project's lint and typecheck commands to verify changes. + +--- + +## Report Format + +```markdown +## Implementation Complete + +### Files Modified + +- `src/components/Feature.tsx` - New component +- `src/hooks/useFeature.ts` - New hook + +### Implementation Summary + +1. Created Feature component... +2. Added useFeature hook... + +### Verification Results + +- Lint: Passed +- TypeCheck: Passed +``` + +--- + +## Code Standards + +- Follow existing code patterns +- Don't add unnecessary abstractions +- Only do what's required, no over-engineering +- Keep code readable diff --git a/.claude/agents/trellis-research.md b/.claude/agents/trellis-research.md new file mode 100644 index 0000000..916686c --- /dev/null +++ b/.claude/agents/trellis-research.md @@ -0,0 +1,137 @@ +--- +name: trellis-research +description: | + Code and tech search expert. Finds files, patterns, and tech solutions, and PERSISTS every finding to the current task's research/ directory. No code modifications outside that directory. +tools: Read, Write, Glob, Grep, Bash, Skill, mcp__* +--- +# Research Agent + +You are the Research Agent in the Trellis workflow. + +## Core Principle + +**You do one thing: find, explain, and PERSIST information.** + +Conversations get compacted; files don't. Every research output MUST end up as a file under `{TASK_DIR}/research/`. Returning findings only through the chat reply is a failure — the caller cannot read them next session. + +--- + +## Core Responsibilities + +1. **Internal Search** — locate files/components, understand code logic, discover patterns (Glob, Grep, Read) +2. **External Search** — library docs, API references, best practices (web search) +3. **Persist** — write each research topic to `{TASK_DIR}/research/<topic>.md` +4. **Report** — return file paths + one-line summaries to the main agent (not full content) + +--- + +## Workflow + +### Step 1: Resolve Current Task + +Run `python3 ./.trellis/scripts/task.py current --source` → active task path. If no active task is set, ask the user where to write output; do NOT guess. + +Ensure `{TASK_DIR}/research/` exists: + +```bash +mkdir -p <TASK_DIR>/research +``` + +### Step 2: Understand Search Request + +Classify: internal / external / mixed. Determine scope (global / specific directory) and expected shape (file list / pattern notes / tech comparison). + +### Step 3: Execute Search + +Run independent searches in parallel (Glob + Grep + web) for efficiency. + +### Step 4: Persist Each Topic + +For each distinct research topic, Write a markdown file at `{TASK_DIR}/research/<topic-slug>.md`. Use the File Format below. + +### Step 5: Report to Main Agent + +Reply with ONLY: + +- List of files written (paths relative to repo root) +- One-line summary per file +- Any critical caveats that the main agent needs to know right now + +Do NOT paste full research content into the reply. The files are the contract. + +--- + +## Scope Limits (Strict) + +### Write ALLOWED + +- `{TASK_DIR}/research/*.md` — your own output +- Creating `{TASK_DIR}/research/` if it doesn't exist (via `mkdir -p`) + +### Write FORBIDDEN + +- Code files (`src/`, `lib/`, …) +- Spec files (`.trellis/spec/`) — main agent should use `update-spec` skill instead +- `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.claude/`, `.cursor/`, etc.) +- Other task directories +- Any git operation (commit / push / branch / merge) + +If the user asks you to edit code, decline and suggest spawning `implement` instead. + +--- + +## File Format + +Each `{TASK_DIR}/research/<topic>.md` should follow: + +```markdown +# Research: <topic> + +- **Query**: <original query> +- **Scope**: <internal / external / mixed> +- **Date**: <YYYY-MM-DD> + +## Findings + +### Files Found + +| File Path | Description | +|---|---| +| `src/services/xxx.ts` | Main implementation | +| `src/types/xxx.ts` | Type definitions | + +### Code Patterns + +<describe patterns, cite file:line> + +### External References + +- [Library X docs](url) — <why relevant, version constraints> + +### Related Specs + +- `.trellis/spec/xxx.md` — <description> + +## Caveats / Not Found + +<anything incomplete or uncertain> +``` + +--- + +## Guidelines + +### DO + +- Provide specific file paths and line numbers +- Quote actual code snippets +- Persist every topic to its own file +- Return file paths in your reply, not the full content +- Mark "not found" explicitly when searches come up empty + +### DON'T + +- Don't write code or modify files outside `{TASK_DIR}/research/` +- Don't guess uncertain info +- Don't paste full research text into the reply (files are the deliverable) +- Don't propose improvements or critique implementation (that's not your role) diff --git a/.claude/commands/trellis/continue.md b/.claude/commands/trellis/continue.md new file mode 100644 index 0000000..1f7a9e6 --- /dev/null +++ b/.claude/commands/trellis/continue.md @@ -0,0 +1,56 @@ +# Continue Current Task + +Resume work on the current task — pick up at the right phase/step in `.trellis/workflow.md`. + +--- + +## Step 1: Load Current Context + +```bash +python3 ./.trellis/scripts/get_context.py +``` + +Confirms: current task, git state, recent commits. + +## Step 2: Load the Phase Index + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase +``` + +Shows the Phase Index (Plan / Execute / Finish) with routing + skill mapping. + +## Step 3: Decide Where You Are + +`get_context.py` shows the active task's `status` field. Route by `status` + artifact presence. 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` 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.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 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 + +Once you know which step to resume at: + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase --step <X.X> --platform claude +``` + +Follow the loaded instructions. After each `[required]` step completes, move to the next. + +--- + +## Reference + +Full workflow and detailed phase steps live in `.trellis/workflow.md`. This command is only an entry point — the canonical guidance is there. diff --git a/.claude/commands/trellis/finish-work.md b/.claude/commands/trellis/finish-work.md new file mode 100644 index 0000000..ab751c6 --- /dev/null +++ b/.claude/commands/trellis/finish-work.md @@ -0,0 +1,66 @@ +# Finish Work + +Wrap up the current session: archive the active task (and any other completed-but-unarchived tasks the user wants to clean up) and record the session journal. Code commits are NOT done here — those happen in workflow Phase 3.4 before you invoke this command. + +## Step 1: Survey current state + +```bash +python3 ./.trellis/scripts/get_context.py --mode record +``` + +This prints: + +- **My active tasks** — review whether any besides the current one are actually done (code merged, AC met) and should be archived this round. +- **Git status** — quick visual on what's dirty. +- **Recent commits** — you'll need their hashes in Step 4 for `--commit`. + +If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless. + +## Step 2: Sanity check — classify dirty paths + +Run: + +```bash +git status --porcelain +``` + +Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this skill's own work. + +For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics: + +- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task +- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task +- Paths in unrelated areas you have no recollection of touching this session → other parallel work + +Then route: + +- **Any remaining path looks like current-task work** — bail out with: + > "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running `/trellis:finish-work`." + + Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there. +- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3: + > "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`." +- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer. + +## Step 3: Archive task(s) + +```bash +python3 ./.trellis/scripts/task.py archive <task-name> +``` + +At minimum: the current active task (if any). Plus any extra tasks the user confirmed in Step 1. Each archive produces a `chore(task): archive ...` commit via the script's auto-commit. + +If there is no active task and the user did not confirm any cleanup archives, skip this step. + +## Step 4: Record session journal + +```bash +python3 ./.trellis/scripts/add_session.py \ + --title "Session Title" \ + --commit "hash1,hash2" \ + --summary "Brief summary" +``` + +Use the work-commit hashes produced in Phase 3.4 (visible in Step 1's `Recent commits` list, or via `git log --oneline`) for `--commit`. Do not include the archive commit hashes from Step 3. This produces a `chore: record journal` commit. + +Final git log order: `<work commits from 3.4>` → `chore(task): archive ...` (one or more) → `chore: record journal`. diff --git a/.claude/hooks/inject-subagent-context.py b/.claude/hooks/inject-subagent-context.py new file mode 100644 index 0000000..975babc --- /dev/null +++ b/.claude/hooks/inject-subagent-context.py @@ -0,0 +1,771 @@ +#!/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 isinstance(input_data.get("cursor_version"), str): + return "cursor" + env_map = { + "CLAUDE_PROJECT_DIR": "claude", + "CURSOR_PROJECT_DIR": "cursor", + "CODEBUDDY_PROJECT_DIR": "codebuddy", + "FACTORY_PROJECT_DIR": "droid", + "GEMINI_PROJECT_DIR": "gemini", + "QODER_PROJECT_DIR": "qoder", + "KIRO_PROJECT_DIR": "kiro", + "COPILOT_PROJECT_DIR": "copilot", + } + for env_name, platform in env_map.items(): + if os.environ.get(env_name): + return platform + script_parts = set(Path(sys.argv[0]).parts) + if ".claude" in script_parts: + return "claude" + if ".cursor" in script_parts: + return "cursor" + if ".gemini" in script_parts: + return "gemini" + if ".qoder" in script_parts: + return "qoder" + if ".codebuddy" in script_parts: + return "codebuddy" + if ".factory" in script_parts: + return "droid" + if ".kiro" in script_parts: + return "kiro" + return None + + +def get_current_task(repo_root: str, input_data: dict) -> str | None: + """Resolve current task directory through the unified active task resolver.""" + scripts_dir = Path(repo_root) / DIR_WORKFLOW / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + try: + from common.active_task import resolve_active_task # type: ignore[import-not-found] + except Exception: + return None + + active = resolve_active_task( + Path(repo_root), + input_data, + platform=_detect_platform(input_data), + ) + return active.task_path + + +def read_file_content(base_path: str, file_path: str) -> str | None: + """Read file content, return None if file doesn't exist""" + full_path = os.path.join(base_path, file_path) + if os.path.exists(full_path) and os.path.isfile(full_path): + try: + with open(full_path, "r", encoding="utf-8") as f: + return f.read() + except Exception: + return None + return None + + +def read_directory_contents( + base_path: str, dir_path: str, max_files: int = 20 +) -> list[tuple[str, str]]: + """ + Read all .md files in a directory + + Args: + base_path: Base path (usually repo_root) + dir_path: Directory relative path + max_files: Max files to read (prevent huge directories) + + Returns: + [(file_path, content), ...] + """ + full_path = os.path.join(base_path, dir_path) + if not os.path.exists(full_path) or not os.path.isdir(full_path): + return [] + + results = [] + try: + # Only read .md files, sorted by filename + md_files = sorted( + [ + f + for f in os.listdir(full_path) + if f.endswith(".md") and os.path.isfile(os.path.join(full_path, f)) + ] + ) + + for filename in md_files[:max_files]: + file_full_path = os.path.join(full_path, filename) + relative_path = os.path.join(dir_path, filename) + try: + with open(file_full_path, "r", encoding="utf-8") as f: + content = f.read() + results.append((relative_path, content)) + except Exception: + continue + except Exception: + pass + + return results + + +def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]: + """ + Read all file/directory contents referenced in jsonl file + + Schema: + {"file": "path/to/file.md", "reason": "..."} + {"file": "path/to/dir/", "type": "directory", "reason": "..."} + {"_example": "..."} # seed row — skipped (no `file` field) + + Rows without a ``file`` field (e.g. the self-describing seed line written + by ``task.py create`` before the agent has curated entries) are skipped + silently. If the resulting entry list is empty, a stderr warning is + emitted so the operator can debug missing context. + + Returns: + [(path, content), ...] + """ + full_path = os.path.join(base_path, jsonl_path) + if not os.path.exists(full_path): + print( + f"[inject-subagent-context] WARN: {jsonl_path} not found — " + f"sub-agent will receive only task artifacts", + file=sys.stderr, + ) + return [] + + results = [] + saw_real_entry = False + try: + with open(full_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + item = json.loads(line) + file_path = item.get("file") or item.get("path") + entry_type = item.get("type", "file") + + if not file_path: + # Seed / comment row — skip silently + continue + + saw_real_entry = True + if entry_type == "directory": + # Read all .md files in directory + dir_contents = read_directory_contents(base_path, file_path) + results.extend(dir_contents) + else: + # Read single file + content = read_file_content(base_path, file_path) + if content: + results.append((file_path, content)) + except json.JSONDecodeError: + continue + except Exception: + pass + + if not saw_real_entry: + print( + f"[inject-subagent-context] WARN: {jsonl_path} has no curated " + f"entries (only seed / empty) — sub-agent will receive only " + f"task artifacts. See workflow.md planning artifact guidance.", + file=sys.stderr, + ) + + return results + + + + +def get_agent_context(repo_root: str, task_dir: str, agent_type: str) -> str: + """ + Get context from {agent_type}.jsonl for the specified agent. + Only reads implement.jsonl or check.jsonl (the two JSONL files the task system creates). + """ + context_parts = [] + + agent_jsonl = f"{task_dir}/{agent_type}.jsonl" + for file_path, content in read_jsonl_entries(repo_root, agent_jsonl): + context_parts.append(f"=== {file_path} ===\n{content}") + + return "\n\n".join(context_parts) + + +def get_implement_context(repo_root: str, task_dir: str) -> str: + """ + Complete context for Implement Agent + + Read order: + 1. All files in implement.jsonl (spec/research manifests) + 2. prd.md (requirements) + 3. design.md if present (technical design) + 4. implement.md if present (execution plan) + """ + context_parts = [] + + # 1. Read implement.jsonl + base_context = get_agent_context(repo_root, task_dir, "implement") + if base_context: + context_parts.append(base_context) + + # 2. Requirements document + prd_content = read_file_content(repo_root, f"{task_dir}/prd.md") + if prd_content: + context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}") + + # 3. Technical design for complex tasks + design_content = read_file_content(repo_root, f"{task_dir}/design.md") + if design_content: + context_parts.append( + f"=== {task_dir}/design.md (Technical Design) ===\n{design_content}" + ) + + # 4. Execution plan for complex tasks + implement_plan_content = read_file_content(repo_root, f"{task_dir}/implement.md") + if implement_plan_content: + context_parts.append( + f"=== {task_dir}/implement.md (Execution Plan) ===\n{implement_plan_content}" + ) + + 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. + """ + context_parts = [] + + for file_path, content in read_jsonl_entries(repo_root, f"{task_dir}/check.jsonl"): + context_parts.append(f"=== {file_path} ===\n{content}") + + prd_content = read_file_content(repo_root, f"{task_dir}/prd.md") + if prd_content: + context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}") + + design_content = read_file_content(repo_root, f"{task_dir}/design.md") + if design_content: + context_parts.append( + f"=== {task_dir}/design.md (Technical Design) ===\n{design_content}" + ) + + implement_plan_content = read_file_content(repo_root, f"{task_dir}/implement.md") + if implement_plan_content: + context_parts.append( + f"=== {task_dir}/implement.md (Execution Plan) ===\n{implement_plan_content}" + ) + + return "\n\n".join(context_parts) + + +def get_finish_context(repo_root: str, task_dir: str) -> str: + """ + Context for Finish phase: reuses check.jsonl + prd.md + (Finish is a final check, same context source.) + """ + return get_check_context(repo_root, task_dir) + + + +def build_implement_prompt(original_prompt: str, context: str) -> str: + """Build complete prompt for Implement""" + return f"""<!-- trellis-hook-injected --> +# Implement Agent Task + +You are the Implement Agent in the Multi-Agent Pipeline. + +## Your Context + +All the information you need has been prepared for you: + +{context} + +--- + +## Your Task + +{original_prompt} + +--- + +## Workflow + +1. **Understand specs** - All dev specs are injected above, understand them + 2. **Understand 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 _extract_subagent_name(value: Any) -> str: + """Extract a sub-agent name from common platform encodings. + + Cursor's native Task args encode custom sub-agents as a protobuf oneof, + which can appear in hook JSON as either ``{"custom": {"name": "..."}}`` + or ``{"type": {"case": "custom", "value": {"name": "..."}}}``. + """ + direct = _string_value(value) + if direct: + return direct + + if not isinstance(value, dict): + return "" + + for key in ("name", "subagent_type_name", "subagentTypeName"): + direct = _string_value(value.get(key)) + if direct: + return direct + + custom = value.get("custom") + if isinstance(custom, dict): + custom_name = _string_value(custom.get("name")) + if custom_name: + return custom_name + + oneof = value.get("type") + if isinstance(oneof, dict): + case_name = _string_value(oneof.get("case")) + if case_name == "custom": + nested_value = oneof.get("value") + if isinstance(nested_value, dict): + custom_name = _string_value(nested_value.get("name")) + if custom_name: + return custom_name + if case_name: + return case_name + + case_name = _string_value(value.get("case")) + if case_name == "custom": + nested_value = value.get("value") + if isinstance(nested_value, dict): + custom_name = _string_value(nested_value.get("name")) + if custom_name: + return custom_name + if case_name: + return case_name + + for agent_name in AGENTS_ALL: + if agent_name in value: + return agent_name + + return "" + + +def _extract_subagent_type(tool_input: dict) -> str: + for key in ( + "subagent_type", + "subagentType", + "subagent_type_name", + "subagentTypeName", + "agent_type", + "agentType", + "name", + ): + agent_name = _extract_subagent_name(tool_input.get(key)) + if agent_name: + return agent_name + return "" + + +def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]: + """Parse hook input across different platform formats. + + Returns (subagent_type, original_prompt, tool_input). + Handles: + - Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type + - Cursor: tool_name=Task|Subagent, tool_input.subagent_type + - Copilot CLI: toolName=task (camelCase key, lowercase value) + - Gemini CLI: tool_name IS the agent name (BeforeTool matcher already filtered) + - Kiro: agentSpawn hook, agent_name field at top level + """ + tool_input = input_data.get("tool_input", {}) + + # Standard format: Task/Agent tool with subagent_type + tool_name = input_data.get("tool_name", "") or input_data.get("toolName", "") + if tool_name.lower() in ("task", "agent", "subagent"): + return ( + _extract_subagent_type(tool_input), + tool_input.get("prompt", ""), + tool_input, + ) + + # Kiro: agentSpawn hook passes agent_name at top level + agent_name = input_data.get("agent_name", "") + if agent_name: + return agent_name, tool_input.get("prompt", input_data.get("prompt", "")), tool_input + + # Gemini CLI: BeforeTool where tool_name IS the agent name + # (matcher already ensured it's one of our agents) + if tool_name in AGENTS_ALL: + return tool_name, tool_input.get("prompt", ""), tool_input + + # Copilot CLI: toolName field (camelCase), value might be the agent name + tool_name_camel = input_data.get("toolName", "") + if tool_name_camel in AGENTS_ALL: + return tool_name_camel, input_data.get("toolArgs", ""), tool_input + + return "", "", tool_input + + +def main(): + if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": + sys.exit(0) + + try: + input_data = json.load(sys.stdin) + except json.JSONDecodeError: + sys.exit(0) + + subagent_type, original_prompt, tool_input = _parse_hook_input(input_data) + cwd = input_data.get("cwd", os.getcwd()) + + # Only handle subagent types we care about + if subagent_type not in AGENTS_ALL: + sys.exit(0) + + # Find repo root + repo_root = find_repo_root(cwd) + if not repo_root: + sys.exit(0) + + # Get current task directory (research doesn't require it) + task_dir = get_current_task(repo_root, input_data) + + # implement/check need task directory + if subagent_type in AGENTS_REQUIRE_TASK: + if not task_dir: + sys.exit(0) + # Check if task directory exists + task_dir_full = os.path.join(repo_root, task_dir) + if not os.path.exists(task_dir_full): + sys.exit(0) + + # Check for [finish] marker in prompt (check agent with finish context) + is_finish_phase = "[finish]" in original_prompt.lower() + + # Get context and build prompt based on subagent type + if subagent_type == AGENT_IMPLEMENT: + assert task_dir is not None # validated above + context = get_implement_context(repo_root, task_dir) + new_prompt = build_implement_prompt(original_prompt, context) + elif subagent_type == AGENT_CHECK: + assert task_dir is not None # validated above + if is_finish_phase: + # Finish phase: use finish context (lighter, focused on final verification) + context = get_finish_context(repo_root, task_dir) + new_prompt = build_finish_prompt(original_prompt, context) + else: + # Regular check phase: use check context (full specs for self-fix loop) + context = get_check_context(repo_root, task_dir) + new_prompt = build_check_prompt(original_prompt, context) + elif subagent_type == AGENT_RESEARCH: + # Research can work without task directory + context = get_research_context(repo_root, task_dir) + new_prompt = build_research_prompt(original_prompt, context) + else: + sys.exit(0) + + if not context: + sys.exit(0) + + # Return updated input — use a multi-format output that covers all platforms. + # Most platforms ignore unrecognized fields, so we include multiple formats. + # The platform picks whichever fields it understands. + updated = {**tool_input, "prompt": new_prompt} + output = { + # Claude Code / Qoder / CodeBuddy / Droid format + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow", + "updatedInput": updated, + }, + # Cursor format + "permission": "allow", + "updated_input": updated, + # Gemini format + "updatedInput": updated, + } + + print(json.dumps(output, ensure_ascii=False)) + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/.claude/hooks/inject-workflow-state.py b/.claude/hooks/inject-workflow-state.py new file mode 100644 index 0000000..e8ab972 --- /dev/null +++ b/.claude/hooks/inject-workflow-state.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +"""Trellis per-turn breadcrumb hook (UserPromptSubmit / BeforeAgent equivalent). + +Runs on every user prompt. Resolves the active task through Trellis' +session-aware active task resolver and emits a short <workflow-state> +block reminding the main AI what task is active and its expected flow. + +The emitted ``hookEventName`` field is platform-aware: most hosts expect +``UserPromptSubmit`` (Claude Code naming, also accepted by Cursor / Qoder / +CodeBuddy / Droid / Codex / Copilot wiring), but Gemini CLI 0.40.x renamed +its per-turn event to ``BeforeAgent`` and its schema validator rejects the +legacy name. ``_detect_platform`` picks the right value at runtime. +Breadcrumb text is pulled exclusively from workflow.md +[workflow-state:STATUS] tag blocks — workflow.md is the single source of +truth. There are no fallback dicts in this script: when workflow.md is +missing or a tag is absent, the breadcrumb degrades to a generic +"Refer to workflow.md for current step." line so users see (and fix) +the broken state instead of the hook silently masking it. + +Shared across all hook-capable platforms (Claude, Cursor, Codex, Qoder, +CodeBuddy, Droid, Gemini, Copilot, Kiro). 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) + - task.json malformed or missing status +""" +from __future__ import annotations + +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 +# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets) +# both in stdin (hook payload from host CLI) and stdout (our emitted blocks) +# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8` +# but applied per-stream so we don't depend on host CLI's command wiring. +if sys.platform.startswith("win"): + import io as _io + for _stream_name in ("stdin", "stdout", "stderr"): + _stream = getattr(sys, _stream_name, None) + if _stream is None: + continue + if hasattr(_stream, "reconfigure"): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] + except Exception: + pass + elif hasattr(_stream, "detach"): + try: + setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) + except Exception: + pass +from typing import Optional + + +# 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> +If you have not already loaded Trellis context this session, read the `trellis-start` skill once. +</trellis-bootstrap>""" + + +# --------------------------------------------------------------------------- +# CWD-robust Trellis root discovery (fixes hook-path-robustness for this hook) +# --------------------------------------------------------------------------- + +def find_trellis_root(start: Path) -> Optional[Path]: + """Walk up from start to find directory containing .trellis/. + + Handles CWD drift: subdirectory launches, monorepo packages, etc. + Returns None if no .trellis/ found (silent no-op). + """ + cur = start.resolve() + while cur != cur.parent: + if (cur / ".trellis").is_dir(): + return cur + cur = cur.parent + return None + + +# --------------------------------------------------------------------------- +# Active task discovery +# --------------------------------------------------------------------------- + +def _detect_platform(input_data: dict) -> str | None: + if isinstance(input_data.get("cursor_version"), str): + return "cursor" + env_map = { + "CLAUDE_PROJECT_DIR": "claude", + "CURSOR_PROJECT_DIR": "cursor", + "CODEBUDDY_PROJECT_DIR": "codebuddy", + "FACTORY_PROJECT_DIR": "droid", + "GEMINI_PROJECT_DIR": "gemini", + "QODER_PROJECT_DIR": "qoder", + "KIRO_PROJECT_DIR": "kiro", + "COPILOT_PROJECT_DIR": "copilot", + "TRAE_PROJECT_DIR": "trae", + } + for env_name, platform in env_map.items(): + if os.environ.get(env_name): + return platform + script_parts = set(Path(sys.argv[0]).parts) + if ".claude" in script_parts: + return "claude" + if ".cursor" in script_parts: + return "cursor" + if ".codex" in script_parts: + return "codex" + if ".gemini" in script_parts: + return "gemini" + if ".qoder" in script_parts: + return "qoder" + if ".codebuddy" in script_parts: + return "codebuddy" + if ".factory" in script_parts: + return "droid" + if ".kiro" in script_parts: + return "kiro" + if ".trae" in script_parts: + return "trae" + return None + + +def _resolve_active_task(root: Path, input_data: dict): + scripts_dir = root / ".trellis" / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_active_task # type: ignore[import-not-found] + + return resolve_active_task(root, input_data, platform=_detect_platform(input_data)) + + +def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, str]]: + """Return (task_id, status, source) from the current active task.""" + active = _resolve_active_task(root, input_data) + if not active.task_path: + return None + + task_dir = Path(active.task_path) + if not task_dir.is_absolute(): + task_dir = root / task_dir + if active.stale: + return task_dir.name, f"stale_{active.source_type}", active.source + + task_json = task_dir / "task.json" + if not task_json.is_file(): + return None + try: + data = json.loads(task_json.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return None + + task_id = data.get("id") or task_dir.name + status = data.get("status", "") + if not isinstance(status, str) or not status: + return None + return task_id, status, active.source + + +# --------------------------------------------------------------------------- +# Breadcrumb loading: parse workflow.md, fall back to hardcoded defaults +# --------------------------------------------------------------------------- + +# Supports STATUS values with letters, digits, underscores, hyphens +# (so "in-review" / "blocked-by-team" work alongside "in_progress"). +_TAG_RE = re.compile( + r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n(.*?)\n\s*\[/workflow-state:\1\]", + re.DOTALL, +) + +def load_breadcrumbs(root: Path) -> dict[str, str]: + """Parse workflow.md for [workflow-state:STATUS] blocks. + + Returns {status: body_text}. workflow.md is the single source of + truth — there are no fallback dicts in this script. Missing tags + (or a missing/unreadable workflow.md) fall back to a generic line + in build_breadcrumb so users see the broken state and fix + workflow.md, rather than the hook silently masking the issue. + """ + workflow = root / ".trellis" / "workflow.md" + if not workflow.is_file(): + return {} + try: + content = workflow.read_text(encoding="utf-8") + except OSError: + return {} + + result: dict[str, str] = {} + for match in _TAG_RE.finditer(content): + status = match.group(1) + body = match.group(2).strip() + if body: + result[status] = body + return result + + +def _read_trellis_config(root: Path) -> dict: + """Load .trellis/config.yaml via the bundled trellis_config helper. + + The helper lives in .trellis/scripts/common; the hook lives outside the + scripts tree, so we extend sys.path before importing. + """ + scripts_dir = root / ".trellis" / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + try: + from common.trellis_config import read_trellis_config # type: ignore[import-not-found] + except Exception: + return {} + try: + return read_trellis_config(root) + except Exception: + return {} + + +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. + """ + 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 + if mode == "sub-agent": + meaning = ( + "sub-agent: implement/check work defaults to Trellis sub-agents; " + "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( + status: str, platform: str | None, config: dict +) -> 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. + + 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 + return f"{status}-inline" if mode == "inline" else status + return status + + +def build_breadcrumb( + task_id: Optional[str], + status: str, + templates: dict[str, str], + source: str | None = None, + breadcrumb_key: str | None = None, +) -> str: + """Build the <workflow-state>...</workflow-state> block. + + - Known status (tag present in workflow.md) → detailed template body + - Unknown status (no tag, or workflow.md missing) → generic + "Refer to workflow.md for current step." line + - `no_task` pseudo-status (task_id is None) → header omits task info + """ + lookup_key = breadcrumb_key or status + body = templates.get(lookup_key) + if body is None and lookup_key != status: + body = templates.get(status) + if body is None: + body = "Refer to workflow.md for current step." + header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})" + return f"<workflow-state>\n{header}\n{body}\n</workflow-state>" + + +# --------------------------------------------------------------------------- +# 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 | BaseException]" = queue.Queue(maxsize=1) + + def _read() -> None: + try: + result_queue.put(sys.stdin.read()) + except BaseException 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, BaseException): + 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 + + data = _load_hook_input() + + cwd_str = data.get("cwd") or os.getcwd() + cwd = Path(cwd_str) + + root = find_trellis_root(cwd) + if root is None: + return 0 # not a Trellis project + + templates = load_breadcrumbs(root) + 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 + # trellis-brainstorm + task.py create when user describes real work. + no_task_key = resolve_breadcrumb_key("no_task", platform, config) + breadcrumb = build_breadcrumb( + None, "no_task", templates, breadcrumb_key=no_task_key + ) + 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_for_breadcrumb, breadcrumb_key=status_key + ) + if platform == "codex": + 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. + hook_event_name = ( + "BeforeAgent" if platform == "gemini" else "UserPromptSubmit" + ) + + output = { + "hookSpecificOutput": { + "hookEventName": hook_event_name, + "additionalContext": breadcrumb, + } + } + print(json.dumps(output)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/hooks/session-start.py b/.claude/hooks/session-start.py new file mode 100644 index 0000000..a7f28bf --- /dev/null +++ b/.claude/hooks/session-start.py @@ -0,0 +1,844 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Session Start Hook - Inject structured context +""" +from __future__ import annotations + +# IMPORTANT: Suppress all warnings FIRST +import warnings +warnings.filterwarnings("ignore") + +import json +import os +import re +import shlex +import subprocess +import sys +from io import StringIO +from pathlib import Path + + +def _normalize_windows_shell_path(path_str: str) -> str: + """Normalize Unix-style shell paths to real Windows paths. + + On Windows, shells like Git Bash / MSYS2 / Cygwin may report paths like + `/d/Users/...` or `/cygdrive/d/Users/...`. `Path.resolve()` will misinterpret + these as `D:/d/Users...` on drive D: (or similar), breaking repo root + detection. + + This function is intentionally conservative: it only rewrites patterns that + unambiguously represent a drive letter mount. + """ + if not isinstance(path_str, str) or not path_str: + return path_str + + # Only relevant on Windows; keep other platforms untouched. + if not sys.platform.startswith("win"): + return path_str + + p = path_str.strip() + + # Already a Windows drive path (C:\... or C:/...) + if re.match(r"^[A-Za-z]:[\/]", p): + return p + + # MSYS/Git-Bash style: /c/Users/... or /d/Work/... + m = re.match(r"^/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + # Cygwin style: /cygdrive/c/Users/... + m = re.match(r"^/cygdrive/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + # WSL mounted drive (sometimes leaked into env): /mnt/c/Users/... + m = re.match(r"^/mnt/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + return path_str + + +FIRST_REPLY_NOTICE = """<first-reply-notice> +First visible reply: say once in Chinese that Trellis SessionStart context is loaded, then answer directly. +This notice is one-shot: do not repeat it after the first assistant reply in the same session. +</first-reply-notice>""" + +# Force UTF-8 on stdin/stdout/stderr on Windows. Default codepage there is +# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets) +# both in stdin (hook payload from host CLI) and stdout (our emitted blocks) +# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8` +# but applied per-stream so we don't depend on host CLI's command wiring. +if sys.platform.startswith("win"): + import io as _io + for _stream_name in ("stdin", "stdout", "stderr"): + _stream = getattr(sys, _stream_name, None) + if _stream is None: + continue + if hasattr(_stream, "reconfigure"): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] + except Exception: + pass + elif hasattr(_stream, "detach"): + try: + setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) + except Exception: + pass + + + +def _has_curated_jsonl_entry(jsonl_path: Path) -> bool: + """Return True iff jsonl has at least one row with a ``file`` field. + + A freshly seeded jsonl only contains a ``{"_example": ...}`` row (no + ``file`` key) — that is NOT "ready". Readiness requires at least one + curated entry. Matches the contract used by hook-inject and pull-based + sub-agent context loaders. + """ + try: + for line in jsonl_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + row = json.loads(line) + except json.JSONDecodeError: + continue + if isinstance(row, dict) and row.get("file"): + return True + except (OSError, UnicodeDecodeError): + return False + return False + + +def should_skip_injection() -> bool: + """Check if any platform's non-interactive flag is set, or if Trellis + hooks are explicitly disabled via TRELLIS_HOOKS=0 / TRELLIS_DISABLE_HOOKS=1. + """ + if os.environ.get("TRELLIS_HOOKS") == "0": + return True + if os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": + return True + non_interactive_vars = [ + "CLAUDE_NON_INTERACTIVE", + "QODER_NON_INTERACTIVE", + "CODEBUDDY_NON_INTERACTIVE", + "FACTORY_NON_INTERACTIVE", + "CURSOR_NON_INTERACTIVE", + "GEMINI_NON_INTERACTIVE", + "KIRO_NON_INTERACTIVE", + "COPILOT_NON_INTERACTIVE", + "TRAE_NON_INTERACTIVE", + ] + return any(os.environ.get(var) == "1" for var in non_interactive_vars) + + +def read_file(path: Path, fallback: str = "") -> str: + try: + return path.read_text(encoding="utf-8") + except (FileNotFoundError, PermissionError): + return fallback + + +def _repo_relative(repo_root: Path, path: Path) -> str: + try: + return path.relative_to(repo_root).as_posix() + except ValueError: + return str(path) + + +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 _detect_platform(input_data: dict) -> str | None: + if isinstance(input_data.get("cursor_version"), str): + return "cursor" + env_map = { + "CLAUDE_PROJECT_DIR": "claude", + "CURSOR_PROJECT_DIR": "cursor", + "CODEBUDDY_PROJECT_DIR": "codebuddy", + "FACTORY_PROJECT_DIR": "droid", + "GEMINI_PROJECT_DIR": "gemini", + "QODER_PROJECT_DIR": "qoder", + "KIRO_PROJECT_DIR": "kiro", + "COPILOT_PROJECT_DIR": "copilot", + "TRAE_PROJECT_DIR": "trae", + } + for env_name, platform in env_map.items(): + if os.environ.get(env_name): + return platform + script_parts = set(Path(sys.argv[0]).parts) + if ".claude" in script_parts: + return "claude" + if ".cursor" in script_parts: + return "cursor" + if ".codex" in script_parts: + return "codex" + if ".gemini" in script_parts: + return "gemini" + if ".qoder" in script_parts: + return "qoder" + if ".codebuddy" in script_parts: + return "codebuddy" + if ".factory" in script_parts: + return "droid" + if ".kiro" in script_parts: + return "kiro" + if ".trae" in script_parts: + return "trae" + return None + + +def _resolve_context_key(trellis_dir: Path, input_data: dict) -> str | None: + scripts_dir = trellis_dir / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_context_key # type: ignore[import-not-found] + + return resolve_context_key(input_data, platform=_detect_platform(input_data)) + + +def _persist_context_key_for_bash(context_key: str | None) -> None: + """Expose Trellis session identity to later Claude Code Bash commands. + + Claude Code SessionStart hooks can append exports to CLAUDE_ENV_FILE; those + variables are then available to Bash tools in the same conversation. Without + this bridge, `task.py start` has hook stdin during SessionStart but no + session identity when the AI later runs it as a normal shell command. + """ + if not context_key: + return + env_file = os.environ.get("CLAUDE_ENV_FILE") + if not env_file: + return + try: + with open(env_file, "a", encoding="utf-8") as handle: + handle.write(f"export TRELLIS_CONTEXT_ID={shlex.quote(context_key)}\n") + except OSError: + pass + + +def _resolve_active_task(trellis_dir: Path, input_data: dict): + scripts_dir = trellis_dir / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_active_task # type: ignore[import-not-found] + + return resolve_active_task( + trellis_dir.parent, + input_data, + platform=_detect_platform(input_data), + ) + + +def run_script(script_path: Path, context_key: str | None = None) -> str: + try: + if script_path.suffix == ".py": + # Add PYTHONIOENCODING to force UTF-8 in subprocess + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + if context_key: + env["TRELLIS_CONTEXT_ID"] = context_key + cmd = [sys.executable, "-W", "ignore", str(script_path)] + else: + env = os.environ.copy() + if context_key: + env["TRELLIS_CONTEXT_ID"] = context_key + cmd = [str(script_path)] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=5, + cwd=script_path.parent.parent.parent, + env=env, + ) + return result.stdout if result.returncode == 0 else "No context available" + except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError): + return "No context available" + + +def _normalize_task_ref(task_ref: str) -> str: + normalized = task_ref.strip() + if not normalized: + return "" + + path_obj = Path(normalized) + if path_obj.is_absolute(): + return str(path_obj) + + normalized = normalized.replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + + if normalized.startswith("tasks/"): + return f".trellis/{normalized}" + + return normalized + + +def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path: + normalized = _normalize_task_ref(task_ref) + path_obj = Path(normalized) + if path_obj.is_absolute(): + return path_obj + if normalized.startswith(".trellis/"): + return trellis_dir.parent / path_obj + return trellis_dir / "tasks" / path_obj + + +def _get_task_status(trellis_dir: Path, input_data: dict) -> str: + """Return compact active-task status, artifact presence, and next action.""" + active = _resolve_active_task(trellis_dir, input_data) + + if not active.task_path: + return ( + "Status: NO ACTIVE TASK\n" + "Next-Action: Classify the current turn before creating any Trellis task. " + "Simple conversation / small task asks only whether this turn should create a Trellis task. " + "Complex task asks whether task creation and planning are allowed." + ) + + task_ref = active.task_path + task_dir = _resolve_task_dir(trellis_dir, task_ref) + if active.stale or not task_dir.is_dir(): + return ( + f"Status: STALE POINTER\nTask: {task_ref}\n" + f"Next-Action: Run `python3 ./.trellis/scripts/task.py finish` to clear the stale pointer, " + "then ask the user what to work on next." + ) + + task_json_path = task_dir / "task.json" + task_data = {} + if task_json_path.is_file(): + try: + task_data = json.loads(task_json_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, PermissionError): + pass + + task_title = task_data.get("title", task_ref) + task_status = task_data.get("status", "unknown") + artifact_names = ("prd.md", "design.md", "implement.md", "implement.jsonl", "check.jsonl") + present = [name for name in artifact_names if (task_dir / name).is_file()] + if (task_dir / "research").is_dir(): + present.append("research/") + present_line = ", ".join(present) if present else "(none)" + + if task_status == "completed": + return ( + f"Status: COMPLETED\nTask: {task_title}\n" + f"Present: {present_line}\n" + "Next-Action: Run `/trellis:finish-work`. If the working tree is dirty, return to Phase 3.4 first." + ) + + has_prd = (task_dir / "prd.md").is_file() + has_design = (task_dir / "design.md").is_file() + has_implement_plan = (task_dir / "implement.md").is_file() + implement_jsonl = task_dir / "implement.jsonl" + check_jsonl = task_dir / "check.jsonl" + jsonl_ready = ( + (not implement_jsonl.is_file() or _has_curated_jsonl_entry(implement_jsonl)) + and (not check_jsonl.is_file() or _has_curated_jsonl_entry(check_jsonl)) + ) + + if task_status == "planning" and not has_prd: + return ( + f"Status: PLANNING\nTask: {task_title}\n" + f"Present: {present_line}\n" + "Next-Action: Load `trellis-brainstorm` and write `prd.md`. Stay in planning." + ) + + if task_status == "planning": + missing_complex = [ + name for name, exists in ( + ("design.md", has_design), + ("implement.md", has_implement_plan), + ) + if not exists + ] + next_bits: list[str] = [] + if missing_complex: + next_bits.append( + "Lightweight task can request start review with PRD-only; " + f"complex task must add {', '.join(missing_complex)} before start" + ) + else: + next_bits.append("Planning artifacts are present; ask for review before `task.py start`") + if not jsonl_ready: + next_bits.append("curate `implement.jsonl` and `check.jsonl` before sub-agent mode start") + return ( + f"Status: PLANNING\nTask: {task_title}\n" + f"Present: {present_line}\n" + f"Next-Action: {'; '.join(next_bits)}. Do not enter implementation until the user confirms start." + ) + + return ( + f"Status: {str(task_status).upper()}\nTask: {task_title}\n" + f"Present: {present_line}\n" + "Next-Action: Follow the matching per-turn workflow-state. " + "Implementation/check context order is jsonl entries -> `prd.md` -> `design.md if present` -> `implement.md if present`." + ) + + +def _load_trellis_config(trellis_dir: Path, input_data: dict) -> tuple: + """Load Trellis config for session-start decisions. + + Returns: + (is_mono, packages_dict, spec_scope, task_pkg, default_pkg) + """ + scripts_dir = trellis_dir / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + + try: + from common.config import get_default_package, get_packages, get_spec_scope, is_monorepo # type: ignore[import-not-found] + from common.paths import get_current_task # type: ignore[import-not-found] + + repo_root = trellis_dir.parent + is_mono = is_monorepo(repo_root) + packages = get_packages(repo_root) or {} + scope = get_spec_scope(repo_root) + + # Get active task's package + task_pkg = None + current = get_current_task( + repo_root, + input_data, + platform=_detect_platform(input_data), + ) + if current: + task_json = repo_root / current / "task.json" + if task_json.is_file(): + try: + data = json.loads(task_json.read_text(encoding="utf-8")) + if isinstance(data, dict): + tp = data.get("package") + if isinstance(tp, str) and tp: + task_pkg = tp + except (json.JSONDecodeError, OSError): + pass + + default_pkg = get_default_package(repo_root) + return is_mono, packages, scope, task_pkg, default_pkg + except Exception: + return False, {}, None, None, None + + +def _check_legacy_spec(trellis_dir: Path, is_mono: bool, packages: dict) -> str | None: + """Check for legacy spec directory structure in monorepo. + + Returns warning message if legacy structure detected, None otherwise. + """ + if not is_mono or not packages: + return None + + spec_dir = trellis_dir / "spec" + if not spec_dir.is_dir(): + return None + + # Check for legacy flat spec dirs (spec/backend/, spec/frontend/ with index.md) + has_legacy = False + for legacy_name in ("backend", "frontend"): + legacy_dir = spec_dir / legacy_name + if legacy_dir.is_dir() and (legacy_dir / "index.md").is_file(): + has_legacy = True + break + + if not has_legacy: + return None + + # Check which packages are missing spec/<pkg>/ directory + missing = [ + name for name in sorted(packages.keys()) + if not (spec_dir / name).is_dir() + ] + + if not missing: + return None # All packages have spec dirs + + if len(missing) == len(packages): + return ( + f"[!] Legacy spec structure detected: found `spec/backend/` or `spec/frontend/` " + f"but no package-scoped `spec/<package>/` directories.\n" + f"Monorepo packages: {', '.join(sorted(packages.keys()))}\n" + f"Please reorganize: `spec/backend/` -> `spec/<package>/backend/`" + ) + return ( + f"[!] Partial spec migration detected: packages {', '.join(missing)} " + f"still missing `spec/<pkg>/` directory.\n" + f"Please complete migration for all packages." + ) + + +def _resolve_spec_scope( + is_mono: bool, + packages: dict, + scope, + task_pkg: str | None, + default_pkg: str | None, +) -> set | None: + """Resolve which packages should have their specs injected. + + Returns: + Set of package names to include, or None for full scan. + """ + if not is_mono or not packages: + return None # Single-repo: full scan + + if scope is None: + return None # No scope configured: full scan + + if isinstance(scope, str) and scope == "active_task": + if task_pkg and task_pkg in packages: + return {task_pkg} + if default_pkg and default_pkg in packages: + return {default_pkg} + return None # Fallback to full scan + + if isinstance(scope, list): + valid = set() + for entry in scope: + if entry in packages: + valid.add(entry) + else: + print( + f"Warning: spec_scope contains unknown package: {entry}, ignoring", + file=sys.stderr, + ) + + if valid: + # Warn if active task is out of scope + if task_pkg and task_pkg not in valid: + print( + f"Warning: active task package '{task_pkg}' is out of configured spec_scope", + file=sys.stderr, + ) + return valid + + # All entries invalid: fallback chain + print( + "Warning: all spec_scope entries invalid, falling back to task/default/full", + file=sys.stderr, + ) + if task_pkg and task_pkg in packages: + return {task_pkg} + if default_pkg and default_pkg in packages: + return {default_pkg} + return None # Full scan + + return None # Unknown scope type: full scan + + +def _collect_spec_index_paths(trellis_dir: Path, allowed_pkgs: set | None) -> 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 + + if allowed_pkgs is not None and sub.name not in allowed_pkgs: + continue + for nested in sorted(sub.iterdir()): + if not nested.is_dir(): + continue + nested_index = nested / "index.md" + if nested_index.is_file(): + paths.append(f".trellis/spec/{sub.name}/{nested.name}/index.md") + + return paths + + +def _build_compact_current_state( + trellis_dir: Path, + input_data: 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, input_data) + 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 + 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 + + 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`. + + Both parameters are full header lines WITHOUT the `## ` prefix (e.g. "Phase Index"). + Returns empty string if start header is not found. + End header missing → extracts to end of file. + """ + lines = content.splitlines() + start: int | None = None + end: int = len(lines) + start_match = f"## {start_header}" + end_match = f"## {end_header}" + for i, line in enumerate(lines): + stripped = line.strip() + if start is None and stripped == start_match: + start = i + continue + if start is not None and stripped == end_match: + end = i + break + if start is None: + return "" + return "\n".join(lines[start:end]).rstrip() + + +_BREADCRUMB_TAG_RE = re.compile( + r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]", + re.DOTALL, +) + + +def _strip_breadcrumb_tag_blocks(content: str) -> str: + """Remove `[workflow-state:STATUS]...[/workflow-state:STATUS]` blocks. + + The tag blocks live inside `## Phase Index` (since v0.5.0-rc.0, when + they were colocated with their phase summaries) and are consumed by the + UserPromptSubmit hook (`inject-workflow-state.py`). The session-start + payload already covers the full step bodies, so re-inlining the + breadcrumbs here would just duplicate context. + """ + 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_overview(workflow_path: Path) -> str: + """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 - Session Summary", + "Full guide: .trellis/workflow.md. Step detail: `python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y>`.", + "", + ] + + phases = _extract_range(content, "Phase Index", "Phase 1: Plan") + if phases: + out_lines.append(_strip_breadcrumb_tag_blocks(phases).rstrip()) + + return "\n".join(out_lines).rstrip() + + +def main(): + if should_skip_injection(): + sys.exit(0) + + try: + hook_input = json.loads(sys.stdin.read()) + if not isinstance(hook_input, dict): + hook_input = {} + except (json.JSONDecodeError, ValueError): + hook_input = {} + + # Try platform-specific env vars, hook cwd, fallback to cwd + project_dir_env_vars = [ + "CLAUDE_PROJECT_DIR", + "QODER_PROJECT_DIR", + "CODEBUDDY_PROJECT_DIR", + "FACTORY_PROJECT_DIR", + "CURSOR_PROJECT_DIR", + "GEMINI_PROJECT_DIR", + "KIRO_PROJECT_DIR", + "COPILOT_PROJECT_DIR", + "TRAE_PROJECT_DIR", + ] + project_dir = None + for var in project_dir_env_vars: + val = os.environ.get(var) + if val: + project_dir = Path(_normalize_windows_shell_path(val)).resolve() + break + if project_dir is None: + project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve() + + trellis_dir = project_dir / ".trellis" + context_key = _resolve_context_key(trellis_dir, hook_input) + _persist_context_key_for_bash(context_key) + + # Load config for scope filtering and legacy detection + is_mono, packages, scope_config, task_pkg, default_pkg = _load_trellis_config( + trellis_dir, + hook_input, + ) + allowed_pkgs = _resolve_spec_scope(is_mono, packages, scope_config, task_pkg, default_pkg) + + output = StringIO() + + spec_index_paths = _collect_spec_index_paths(trellis_dir, allowed_pkgs) + + output.write("""<session-context> +Trellis compact SessionStart context. Use it to orient the session; load details on demand. +</session-context> + +""") + output.write(FIRST_REPLY_NOTICE) + output.write("\n\n") + + # Legacy migration warning + legacy_warning = _check_legacy_spec(trellis_dir, is_mono, packages) + if legacy_warning: + output.write(f"<migration-warning>\n{legacy_warning}\n</migration-warning>\n\n") + + output.write("<current-state>\n") + output.write(_build_compact_current_state(trellis_dir, hook_input, spec_index_paths)) + output.write("\n</current-state>\n\n") + + output.write("<trellis-workflow>\n") + output.write(_build_workflow_overview(trellis_dir / "workflow.md")) + output.write("\n</trellis-workflow>\n\n") + + output.write("<guidelines>\n") + output.write( + "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" + ) + + 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") + + output.write( + "Discover more via: " + "`python3 ./.trellis/scripts/get_context.py --mode packages`\n" + ) + output.write("</guidelines>\n\n") + + # Check task status and inject structured tag + task_status = _get_task_status(trellis_dir, hook_input) + output.write(f"<task-status>\n{task_status}\n</task-status>\n\n") + + output.write("""<ready> +Context loaded. Follow <task-status>. Load workflow/spec/task details only when needed. +</ready>""") + + context_text = output.getvalue() + + # Kiro (CLI trellis agent agentSpawn) adds a hook's stdout directly to the + # conversation context — no JSON envelope. Emit the bare overview text. + # Conditionally isolated: all other platforms keep the JSON path below. + if _detect_platform(hook_input) == "kiro": + print(context_text, flush=True) + return + + result = { + # Claude Code / Qoder / CodeBuddy / Droid / Gemini / Copilot format + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": context_text, + }, + # Cursor sessionStart format (top-level snake_case per Cursor docs) + "additional_context": context_text, + } + + # Output JSON - stdout is already configured for UTF-8 + print(json.dumps(result, ensure_ascii=False), flush=True) + + +if __name__ == "__main__": + main() diff --git a/.claude/hooks/statusline.py b/.claude/hooks/statusline.py new file mode 100644 index 0000000..eac2994 --- /dev/null +++ b/.claude/hooks/statusline.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Trellis StatusLine — project-level status display for Claude Code. + +Reads Claude Code session JSON from stdin + Trellis task data from filesystem. +Outputs 1-2 lines: + With active task: [P1] Task title (status) + info line + Without task: info line only +Info line: model · ctx% · branch · duration · developer · tasks · rate limits +When COLUMNS (injected by Claude Code v2.1.153+) is too narrow for the info +line, the rate-limit segments move to their own line via an explicit "\n". +""" +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +import time +from datetime import datetime +from pathlib import Path + +# Fix: Windows Python defaults to GBK encoding, which corrupts UTF-8 +# characters like the middle dot (·). Wrap stdout/stderr with UTF-8. +if sys.platform == "win32": + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if callable(reconfigure): + reconfigure(encoding="utf-8", errors="replace") + + +def _read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8").strip() + except (FileNotFoundError, PermissionError, OSError): + return "" + + +def _read_json(path: Path) -> dict: + text = _read_text(path) + if not text: + return {} + try: + return json.loads(text) + except (json.JSONDecodeError, ValueError): + return {} + + +def _normalize_task_ref(task_ref: str) -> str: + normalized = task_ref.strip() + if not normalized: + return "" + + path_obj = Path(normalized) + if path_obj.is_absolute(): + return str(path_obj) + + normalized = normalized.replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + + if normalized.startswith("tasks/"): + return f".trellis/{normalized}" + + return normalized + + +def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path: + normalized = _normalize_task_ref(task_ref) + path_obj = Path(normalized) + if path_obj.is_absolute(): + return path_obj + if normalized.startswith(".trellis/"): + return trellis_dir.parent / path_obj + return trellis_dir / "tasks" / path_obj + + +def _find_trellis_dir() -> Path | None: + """Walk up from cwd to find .trellis/ directory.""" + current = Path.cwd() + for parent in [current, *current.parents]: + candidate = parent / ".trellis" + if candidate.is_dir(): + return candidate + return None + + +def _get_current_task(trellis_dir: Path) -> dict | None: + """Load current task info through Trellis' active task resolver.""" + return _get_current_task_for_input(trellis_dir, {}) + + +def _get_current_task_for_input(trellis_dir: Path, cc_data: dict) -> dict | None: + """Load current task info for the Claude Code session JSON.""" + scripts_dir = trellis_dir / "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(trellis_dir.parent, cc_data, platform="claude") + if not active.task_path: + return None + + task_path = _resolve_task_dir(trellis_dir, active.task_path) + if active.stale: + return { + "title": task_path.name, + "status": "stale", + "priority": "P?", + "source": active.source, + } + + task_data = _read_json(task_path / "task.json") + if not task_data: + return None + + return { + "title": task_data.get("title") or task_data.get("name") or "unknown", + "status": task_data.get("status", "unknown"), + "priority": task_data.get("priority", "P2"), + "source": active.source, + } + + +def _count_active_tasks(trellis_dir: Path) -> int: + """Count non-archived task directories with valid task.json.""" + tasks_dir = trellis_dir / "tasks" + if not tasks_dir.is_dir(): + return 0 + count = 0 + for d in tasks_dir.iterdir(): + if d.is_dir() and d.name != "archive" and (d / "task.json").is_file(): + count += 1 + return count + + +def _get_developer(trellis_dir: Path) -> str: + content = _read_text(trellis_dir / ".developer") + if not content: + return "unknown" + for line in content.splitlines(): + if line.startswith("name="): + return line[5:].strip() + return content.splitlines()[0].strip() or "unknown" + + +def _get_git_branch() -> str: + try: + result = subprocess.run( + ["git", "branch", "--show-current"], + capture_output=True, text=True, timeout=3, + ) + return result.stdout.strip() if result.returncode == 0 else "" + except (FileNotFoundError, subprocess.TimeoutExpired): + return "" + + +def _format_ctx_size(size: int) -> str: + if size >= 1_000_000: + return f"{size // 1_000_000}M" + if size >= 1_000: + return f"{size // 1_000}K" + return str(size) + + +def _format_duration(ms: int) -> str: + secs = ms // 1000 + hours, remainder = divmod(secs, 3600) + mins = remainder // 60 + if hours > 0: + return f"{hours}h{mins}m" + return f"{mins}m" + + +def _format_remaining(secs: int) -> str: + if secs <= 0: + return "" + days, remainder = divmod(secs, 86400) + hours, remainder = divmod(remainder, 3600) + mins = remainder // 60 + if days > 0: + return f"{days}d{hours}h" + if hours > 0: + return f"{hours}h{mins}m" + return f"{mins}m" + + +def _parse_resets_at(value: object) -> int: + """`resets_at` is epoch seconds (int/float, possibly stringified) or an + ISO-8601 timestamp depending on Claude Code version. Return epoch + seconds, or 0 when absent/unparseable (countdown is then omitted).""" + if isinstance(value, (int, float)): + return int(value) + if isinstance(value, str) and value: + try: + return int(float(value)) + except ValueError: + pass + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + return int(parsed.timestamp()) + except ValueError: + pass + return 0 + + +def _rate_limit_part(label: str, window: dict, now: int) -> str: + try: + pct = int(float(window.get("used_percentage"))) # pyright: ignore[reportArgumentType] + except (TypeError, ValueError): + return "" + part = f"{label} {pct}%" + remaining = _format_remaining(_parse_resets_at(window.get("resets_at")) - now) + if remaining: + part += f" \033[90m(reset {remaining})\033[0m" + return part + + +_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") + + +def _visible_len(s: str) -> int: + """Length of s with ANSI escape sequences stripped.""" + return len(_ANSI_RE.sub("", s)) + + +def _terminal_width() -> int | None: + """Terminal width from the COLUMNS env var, or None. + + The statusline stdin JSON has no width field and stdout is a pipe, so + the COLUMNS env var (injected by Claude Code v2.1.153+) is the only + width signal. Absent or malformed values return None.""" + try: + width = int(os.environ.get("COLUMNS", "")) + except ValueError: + return None + return width if width > 0 else None + + +def main() -> None: + # Read Claude Code session JSON from stdin + try: + cc_data = json.loads(sys.stdin.read()) + except (json.JSONDecodeError, ValueError): + cc_data = {} + + trellis_dir = _find_trellis_dir() + SEP = " \033[90m·\033[0m " + + # --- Trellis data --- + task = _get_current_task_for_input(trellis_dir, cc_data) if trellis_dir else None + dev = _get_developer(trellis_dir) if trellis_dir else "" + task_count = _count_active_tasks(trellis_dir) if trellis_dir else 0 + + # --- CC session data --- + model = cc_data.get("model", {}).get("display_name", "?") + ctx_pct = int(cc_data.get("context_window", {}).get("used_percentage") or 0) + ctx_size = _format_ctx_size(cc_data.get("context_window", {}).get("context_window_size") or 0) + duration = _format_duration(cc_data.get("cost", {}).get("total_duration_ms") or 0) + branch = _get_git_branch() + + # Avoid "Opus 4.6 (1M context) (1M)" + if re.search(r"\d+[KMG]\b", model, re.IGNORECASE): + model_label = model + else: + model_label = f"{model} ({ctx_size})" + + # Context % with color + if ctx_pct >= 90: + ctx_color = "\033[31m" + elif ctx_pct >= 70: + ctx_color = "\033[33m" + else: + ctx_color = "\033[32m" + + # Build info line: model · ctx · branch · duration · dev · tasks [· rate limits] + parts = [ + model_label, + f"ctx {ctx_color}{ctx_pct}%\033[0m", + ] + if branch: + parts.append(f"\033[35m{branch}\033[0m") + parts.append(duration) + if dev: + parts.append(f"\033[32m{dev}\033[0m") + if task_count: + parts.append(f"{task_count} task(s)") + + now = int(time.time()) + rate_limits = cc_data.get("rate_limits", {}) + rate_parts: list[str] = [] + for label, key in (("5h", "five_hour"), ("7d", "seven_day")): + part = _rate_limit_part(label, rate_limits.get(key) or {}, now) + if part: + rate_parts.append(part) + + info_line = SEP.join(parts + rate_parts) + + # Output: task line (only if active) + info line + if task: + source = str(task.get("source") or "") + source_tag = "session" if source.startswith("session:") else source + source_suffix = f" \033[90m[{source_tag}]\033[0m" if source_tag else "" + print(f"\033[36m[{task['priority']}]\033[0m {task['title']} \033[33m({task['status']})\033[0m{source_suffix}") + + # Claude Code's status-bar height counts only "\n" characters, so a + # visually wrapped long line misaligns rows. When the host provides a + # terminal width and the info line would overflow, split the rate-limit + # segments onto their own line with an explicit "\n" instead. + width = _terminal_width() + if width is not None and rate_parts and _visible_len(info_line) > width: + print(SEP.join(parts)) + print(SEP.join(rate_parts)) + else: + print(info_line) + + +if __name__ == "__main__": + main() diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..f8a5472 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,77 @@ +{ + "env": { + "CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": "1" + }, + "hooks": { + "SessionStart": [ + { + "matcher": "startup", + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/session-start.py", + "timeout": 30 + } + ] + }, + { + "matcher": "clear", + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/session-start.py", + "timeout": 30 + } + ] + }, + { + "matcher": "compact", + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/session-start.py", + "timeout": 30 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Task", + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/inject-subagent-context.py", + "timeout": 30 + } + ] + }, + { + "matcher": "Agent", + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/inject-subagent-context.py", + "timeout": 30 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/inject-workflow-state.py", + "timeout": 15 + } + ] + } + ] + }, + "enabledPlugins": {}, + "statusLine": { + "type": "command", + "command": "python3 .claude/hooks/statusline.py" + } +} diff --git a/.claude/skills/trellis-before-dev/SKILL.md b/.claude/skills/trellis-before-dev/SKILL.md new file mode 100644 index 0000000..5a4b852 --- /dev/null +++ b/.claude/skills/trellis-before-dev/SKILL.md @@ -0,0 +1,40 @@ +--- +name: trellis-before-dev +description: "Discovers and injects project-specific coding guidelines from .trellis/spec/ before implementation begins. Reads spec indexes, pre-development checklists, and shared thinking guides for the target package. Use when starting a new coding task, before writing any code, switching to a different package, or needing to refresh project conventions and standards." +--- + +Read the relevant development guidelines before starting your task. + +Execute these steps: + +1. **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 + ``` + +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 + +4. **Read the spec index** for each relevant module: + ```bash + cat .trellis/spec/<package>/<layer>/index.md + ``` + Follow the **"Pre-Development Checklist"** section in the index. + +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. + +6. **Always read shared guides**: + ```bash + cat .trellis/spec/guides/index.md + ``` + +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/.claude/skills/trellis-brainstorm/SKILL.md b/.claude/skills/trellis-brainstorm/SKILL.md new file mode 100644 index 0000000..806ee31 --- /dev/null +++ b/.claude/skills/trellis-brainstorm/SKILL.md @@ -0,0 +1,173 @@ +--- +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." +--- + +# Trellis Brainstorm + +## Non-Negotiable Interview Contract + +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. + +Ask the questions one at a time. + +## 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. + +Do not ask the user to confirm facts that the repository can answer. Ask only for product intent, preference, scope, risk tolerance, or decisions that remain ambiguous after inspection. + +--- + +Use this skill during Phase 1 planning to turn the user's request into clear requirements and planning artifacts. + +## Preconditions + +Use this skill only after task-creation consent has been given and the user is ready to enter Trellis planning. + +If no task exists yet, create one: + +```bash +TASK_DIR=$(python3 ./.trellis/scripts/task.py create "<short task title>" --slug <slug>) +``` + +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. + +`task.py create` creates the default `prd.md`. Update that file with the current understanding before asking follow-up questions. + +## Planning Flow + +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. Ask the single highest-value remaining question. +5. Include your recommended answer with the question. +6. After each user answer, update `prd.md` before continuing. +7. For complex tasks, create or update `design.md` and `implement.md` before implementation starts. +8. Before final review or `task.py start`, run the PRD convergence pass below. + +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. + +## Question Rules + +Ask only one question per message. + +Each question must include: + +- the decision needed +- why the answer matters +- your recommended answer +- the trade-off if the user chooses differently + +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. + +## 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. + +### Step 1: Restate the Problem + +Strip away implementation details to one sentence. + +> 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 + +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 + +For each component of the current plan: + +- **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. + +### Step 4: Build Up from Truths + +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?" + +### Step 5: Validate + +- Does the solution solve the original problem? +- What assumptions need verification? +- What's the simplest experiment to test this? + +## Artifact Rules + +`prd.md` records requirements and acceptance: + +- goal and user value +- confirmed facts +- requirements +- acceptance criteria +- out of scope +- open questions that still block planning + +`design.md` records technical design for complex tasks: + +- architecture and boundaries +- data flow and contracts +- compatibility and migration notes +- important trade-offs +- operational or rollback considerations + +`implement.md` records execution planning for complex tasks: + +- ordered implementation checklist +- validation commands +- risky files or rollback points +- follow-up checks before `task.py start` + +Lightweight tasks may have only `prd.md`. Complex tasks must have `prd.md`, `design.md`, and `implement.md` before `task.py start`. + +`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`. + +## PRD Convergence Pass + +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. + +The pass must be lossless: + +- 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. +- Keep only genuinely blocking open questions. + +After the pass, read `prd.md` top to bottom and verify that no fact is repeated across sections unless the repetition adds new information. + +## Quality Bar + +Before declaring planning ready: + +- `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. +- Remaining open questions are genuinely about user intent or scope. +- 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 user has reviewed the final planning artifacts or explicitly approved proceeding. + +Do not start implementation until the user approves or asks for implementation. diff --git a/.claude/skills/trellis-break-loop/SKILL.md b/.claude/skills/trellis-break-loop/SKILL.md new file mode 100644 index 0000000..1c8b397 --- /dev/null +++ b/.claude/skills/trellis-break-loop/SKILL.md @@ -0,0 +1,188 @@ +--- +name: trellis-break-loop +description: "Deep bug analysis to break the fix-forget-repeat cycle. Analyzes root cause category, why fixes failed, prevention mechanisms, and captures knowledge into specs. Use after fixing a bug to prevent the same class of bugs." +--- + +# Break the Loop - Deep Bug Analysis + +When debug is complete, use this for deep analysis to break the "fix bug -> forget -> repeat" cycle. + +--- + +## Analysis Framework + +Analyze the bug you just fixed from these 5 dimensions: + +### 1. Root Cause Category + +Which category does this bug belong to? + +| Category | Characteristics | Example | +|----------|-----------------|---------| +| **A. Missing Spec** | No documentation on how to do it | New feature without checklist | +| **B. Cross-Layer Contract** | Interface between layers unclear | API returns different format than expected | +| **C. Change Propagation Failure** | Changed one place, missed others | Changed function signature, missed call sites | +| **D. Test Coverage Gap** | Unit test passes, integration fails | Works alone, breaks when combined | +| **E. Implicit Assumption** | Code relies on undocumented assumption | Timestamp seconds vs milliseconds | + +### 2. Why Fixes Failed (if applicable) + +If you tried multiple fixes before succeeding, analyze each failure: + +- **Surface Fix**: Fixed symptom, not root cause +- **Incomplete Scope**: Found root cause, didn't cover all cases +- **Tool Limitation**: grep missed it, type check wasn't strict +- **Mental Model**: Kept looking in same layer, didn't think cross-layer + +### 3. Prevention Mechanisms + +What mechanisms would prevent this from happening again? + +| Type | Description | Example | +|------|-------------|---------| +| **Documentation** | Write it down so people know | Update thinking guide | +| **Architecture** | Make the error impossible structurally | Type-safe wrappers | +| **Compile-time** | Strict type checking, no escape hatches | Signature change causes compile error | +| **Runtime** | Monitoring, alerts, scans | Detect orphan entities | +| **Test Coverage** | E2E tests, integration tests | Verify full flow | +| **Code Review** | Checklist, PR template | "Did you check X?" | + +### 4. Systematic Expansion + +What broader problems does this bug reveal? + +- **Similar Issues**: Where else might this problem exist? +- **Design Flaw**: Is there a fundamental architecture issue? +- **Process Flaw**: Is there a development process improvement? +- **Knowledge Gap**: Is the team missing some understanding? + +### 5. Knowledge Capture + +Solidify insights into the system: + +- [ ] Update `.trellis/spec/guides/` thinking guides +- [ ] Update relevant `.trellis/spec/` docs +- [ ] Create issue record (if applicable) +- [ ] Create feature ticket for root fix +- [ ] Update check guidelines if needed + +--- + +## Output Format + +Please output analysis in this format: + +```markdown +## Bug Analysis: [Short Description] + +### 1. Root Cause Category +- **Category**: [A/B/C/D/E] - [Category Name] +- **Specific Cause**: [Detailed description] + +### 2. Why Fixes Failed (if applicable) +1. [First attempt]: [Why it failed] +2. [Second attempt]: [Why it failed] +... + +### 3. Prevention Mechanisms +| Priority | Mechanism | Specific Action | Status | +|----------|-----------|-----------------|--------| +| P0 | ... | ... | TODO/DONE | + +### 4. Systematic Expansion +- **Similar Issues**: [List places with similar problems] +- **Design Improvement**: [Architecture-level suggestions] +- **Process Improvement**: [Development process suggestions] + +### 5. Knowledge Capture +- [ ] [Documents to update / tickets to create] +``` + +--- + +## Core Philosophy + +> **The value of debugging is not in fixing the bug, but in making this class of bugs never happen again.** + +Three levels of insight: +1. **Tactical**: How to fix THIS bug +2. **Strategic**: How to prevent THIS CLASS of bugs +3. **Philosophical**: How to expand thinking patterns + +30 minutes of analysis saves 30 hours of future debugging. + +## 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 + +**IMPORTANT**: After completing the analysis above, you MUST immediately: + +1. **Update spec/guides** - Don't just list TODOs, actually update the relevant files: + - If it's a cross-platform issue → update `cross-platform-thinking-guide.md` + - If it's a cross-layer issue → update `cross-layer-thinking-guide.md` + - If it's a code reuse issue → update `code-reuse-thinking-guide.md` + - If it's domain-specific → update `backend/*.md` or `frontend/*.md` + +2. **Sync templates** - After updating `.trellis/spec/`, sync to `src/templates/markdown/spec/` + +3. **Commit the spec updates** - This is the primary output, not just the analysis text + +> **The analysis is worthless if it stays in chat. The value is in the updated specs.** diff --git a/.claude/skills/trellis-channel/SKILL.md b/.claude/skills/trellis-channel/SKILL.md new file mode 100644 index 0000000..511ee02 --- /dev/null +++ b/.claude/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 <board> --scope global +trellis channel thread <board> <thread> --scope global +trellis channel context list <board> --scope global --thread <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 <my_signal>` 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/.claude/skills/trellis-channel/references/command-reference.md b/.claude/skills/trellis-channel/references/command-reference.md new file mode 100644 index 0000000..75def26 --- /dev/null +++ b/.claude/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 <project|global>` unless noted; `project` +is the default and resolves against the current cwd's project bucket. + +## Top-level + +``` +trellis channel <subcommand> +``` + +> Multi-agent collaboration runtime — spawn / coordinate / interrupt worker +> agents through a shared event log. + +--- + +## Create / List + +### `create <name>` + +```bash +trellis channel create <name> + [--scope project|global] # default: project + [--type chat|forum] # default: chat + [--task <path>] # associated Trellis task dir + [--project <slug>] + [--labels a,b,c] + [--description <text>] # stable channel description + [--context-file <abs-path>] ... # repeatable + [--context-raw <text>] ... # repeatable + [--linked-context-file <abs-path>] # [deprecated alias] + [--linked-context-raw <text>] # [deprecated alias] + [--cwd <path>] # recorded in create event + [--by <agent>] # 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 <slug>] # 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 <name> [text]` + +```bash +trellis channel send <name> [text] + --as <agent> # REQUIRED — author + [--scope project|global] + [--to <agents,csv>] # default: broadcast + [--stdin | --text-file <path>] # 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 <name>` + +```bash +trellis channel messages <name> + [--scope project|global] + [--raw] # one JSON event per line + [--follow] # stream new events + [--last <N>] # last N matching events + [--since <seq>] # seq > N + [--kind <kind>] # one of CHANNEL_EVENT_KINDS + [--from <csv>] # author filter + [--to <target>] # routing target filter + [--thread <key>] # forum-only + [--action <thread-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 <name>` + +```bash +trellis channel wait <name> + --as <agent> # REQUIRED — self for filter ctx + [--scope project|global] + [--timeout <Ns|Nm|Nh|Nms>] # parsed by parseDuration + [--from <a,b>] # author CSV + [--kind <k1,k2>] # CSV, OR semantics + [--thread <key>] # forum filter + [--action <thread-action>] # forum filter + [--to <target>] # 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 '<x>'. 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 <name> [text]` + +```bash +trellis channel interrupt <name> [text] + --as <agent> # REQUIRED — caller + --to <agent> # REQUIRED — target worker + [--scope project|global] + [--stdin | --text-file <path>] +``` + +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 <name>` + +```bash +trellis channel spawn <name> + [--scope project|global] + [--agent <agent-name>] # loads .trellis/agents/<name>.md + [--provider claude|codex] # overrides agent file + [--as <worker-name>] # default: agent name + [--cwd <path>] + [--model <id>] + [--resume <id>] # session/thread id resume + [--timeout <Ns|Nm|Nh>] # auto-kill after duration + [--warn-before <Ns|Nm|Nh>] # supervisor_warning lead time + # default 5m, 0ms disables + [--file <path>] ... # glob, repeatable; inject content + [--jsonl <path>] ... # Trellis manifest, repeatable + [--by <agent>] # spawn-event author + # default: TRELLIS_CHANNEL_AS env or 'main' + [--inbox-policy explicitOnly|broadcastAndExplicit] + # default explicitOnly + [--idle-timeout <Ns|Nm|Nh>] # OOM-guard idle TTL + # default 5m, 0 disables + [--max-live-workers <n>] # 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 <worker>`. +- 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 <name>] + [--provider claude|codex] + [--as <worker-name>] + [--cwd <path>] + [--model <id>] + [--file <path>] ... # repeatable, glob + [--jsonl <path>] ... # repeatable + [--message <text> | --message-file <path> | --stdin] + [--timeout <Ns|Nm|Nh>] # default 5m +``` + +Behavior: +- One-shot. Auto-generates `run-<hex>` 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 <name>` + +```bash +trellis channel kill <name> + --as <agent> # 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 <name>` + +```bash +trellis channel rm <name> + [--scope project|global] +``` + +Behavior: +- Kills any live workers, then deletes the entire channel directory. +- Prints `Removed channel '<name>'`. + +### `prune` + +```bash +trellis channel prune + [--scope project|global] # omitted: scan every project + [--all | --empty | --idle <Ns|Nm|Nh|Nd> | --ephemeral] # mutually exclusive + [--yes] # actually delete (default: dry-run) + [--dry-run] # default true; redundant with default + [--keep <names,csv>] # 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 <name> <action>` + +```bash +trellis channel post <name> <action> + --as <agent> # REQUIRED + [--scope project|global] + [--thread <key>] # required except action=opened + [--title <text>] + [--text <text> | --stdin | --text-file <path>] + [--description <text>] # stable thread description + [--status <status>] + [--labels a,b] # REPLACES thread labels + [--assignees a,b] # REPLACES assignees + [--summary <text>] + [--context-file <abs-path>] ... + [--context-raw <text>] ... + [--linked-context-file <abs-path>] # [deprecated alias] + [--linked-context-raw <text>] # [deprecated alias] +``` + +Behavior: +- `<action>` 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 <name>` + +```bash +trellis channel forum <name> + [--scope project|global] + [--status <status>] + [--raw] +``` + +Behavior: +- Lists threads (reduced state). `--status` filters by current thread + status. `--raw` prints one JSON per thread. + +### `thread <name> <thread>` / `thread rename` + +```bash +trellis channel thread <name> <thread-key> + [--scope project|global] + [--raw] + +trellis channel thread rename <name> <old-thread> <new-thread> + --as <agent> # REQUIRED + [--scope project|global] +``` + +Behavior: +- `thread <name> <key>` shows one thread's timeline: + header `<thread> [<status>] <title>`, 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/.claude/skills/trellis-channel/references/forum.md b/.claude/skills/trellis-channel/references/forum.md new file mode 100644 index 0000000..06b7f36 --- /dev/null +++ b/.claude/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/.claude/skills/trellis-channel/references/progress-debugging.md b/.claude/skills/trellis-channel/references/progress-debugging.md new file mode 100644 index 0000000..3ed40d6 --- /dev/null +++ b/.claude/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/.claude/skills/trellis-channel/references/workers.md b/.claude/skills/trellis-channel/references/workers.md new file mode 100644 index 0000000..bcec98f --- /dev/null +++ b/.claude/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/.claude/skills/trellis-channel/references/workflows.md b/.claude/skills/trellis-channel/references/workflows.md new file mode 100644 index 0000000..3319764 --- /dev/null +++ b/.claude/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/.claude/skills/trellis-check/SKILL.md b/.claude/skills/trellis-check/SKILL.md new file mode 100644 index 0000000..c695abd --- /dev/null +++ b/.claude/skills/trellis-check/SKILL.md @@ -0,0 +1,98 @@ +--- +name: trellis-check +description: "Comprehensive quality verification: spec compliance, lint, type-check, tests, cross-layer data flow, code reuse, and consistency checks. Use when code is written and needs quality verification, before committing changes, or to catch context drift during long sessions." +--- + +# Code Quality Check + +Comprehensive quality verification for recently written code. Combines spec compliance, cross-layer safety, and pre-commit checks. + +--- + +## Step 1: Identify What Changed + +```bash +git diff --name-only HEAD +git status +``` + +## Step 2: Read 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 +``` + +For each changed package/layer, read the spec index and follow its **Quality Check** section: + +```bash +cat .trellis/spec/<package>/<layer>/index.md +``` + +Read the specific guideline files referenced — the index is a pointer, not the goal. + +## Step 3: Run Project Checks + +Run the project's lint, type-check, and test commands. Fix any failures before proceeding. + +## Step 4: Review Against Checklist + +### Code Quality + +- [ ] Linter passes? +- [ ] Type checker passes (if applicable)? +- [ ] Tests pass? +- [ ] No debug logging left in? +- [ ] No suppressed warnings or type-safety bypasses? + +### Test Coverage + +- [ ] New function → unit test added? +- [ ] Bug fix → regression test added? +- [ ] Changed behavior → existing tests updated? + +### Spec Sync + +- [ ] Does `.trellis/spec/` need updates? (new patterns, conventions, lessons learned) + +> "If I fixed a bug or discovered something non-obvious, should I document it so future me won't hit the same issue?" → If YES, update the relevant spec doc. + +## Step 5: Cross-Layer Dimensions (if applicable) + +Skip this step if your change is confined to a single layer. + +### A. Data Flow (changes touch 3+ layers) + +- [ ] Read flow traces correctly: Storage → Service → API → UI +- [ ] Write flow traces correctly: UI → API → Service → Storage +- [ ] Types/schemas correctly passed between layers? +- [ ] Errors properly propagated to caller? + +### B. Code Reuse (modifying constants, creating utilities) + +- [ ] Searched for existing similar code before creating new? + ```bash + grep -r "pattern" src/ + ``` +- [ ] If 2+ places define same value → extracted to shared constant? +- [ ] After batch modification, all occurrences updated? + +### C. Import/Dependency (creating new files) + +- [ ] Correct import paths (relative vs absolute)? +- [ ] No circular dependencies? + +### D. Same-Layer Consistency + +- [ ] Other places using the same concept are consistent? + +--- + +## Step 6: Report and Fix + +Report violations found and fix them directly. Re-run project checks after fixes. diff --git a/.claude/skills/trellis-meta/SKILL.md b/.claude/skills/trellis-meta/SKILL.md new file mode 100644 index 0000000..0754b53 --- /dev/null +++ b/.claude/skills/trellis-meta/SKILL.md @@ -0,0 +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, 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, bundled runtime agents, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.reasonix/`, `.kilocode/`, `.agent/`, `.devin/`, 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. +- 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` — 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 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 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 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 15 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, 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; 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` 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/.claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md b/.claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md new file mode 100644 index 0000000..608aaa6 --- /dev/null +++ b/.claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md @@ -0,0 +1,83 @@ +# Add Project-Local Conventions + +Often the user does not need to change Trellis mechanics; they need local AI to understand their team's conventions. In that case, prefer `.trellis/spec/` or a project-local skill instead of editing `trellis-meta`. + +## Where To Put Things + +| Content type | Location | +| --- | --- | +| Rules code must follow | `.trellis/spec/<layer>/` | +| Cross-layer thinking methods | `.trellis/spec/guides/` | +| AI capability for a project-specific flow | Platform-local skill | +| One-off task material | `.trellis/tasks/<task>/` | +| Session summary | `.trellis/workspace/<developer>/journal-N.md` | + +## Create A Project-Local Skill + +If the user wants AI to know "how this project customizes Trellis," create a local skill: + +```text +.claude/skills/trellis-local/ +└── SKILL.md +``` + +Example: + +```md +--- +name: trellis-local +description: "Project-local Trellis customizations for this repository. Use when changing this project's Trellis workflow, hooks, local agents, or team-specific conventions." +--- + +# Trellis Local + +## Local Scope + +This skill documents this repository's Trellis customizations only. + +## Custom Workflow Rules + +- ... + +## Local Hook Changes + +- ... + +## Local Agent Changes + +- ... +``` + +For multi-platform projects, place equivalent versions in other platform skill directories, or use `.agents/skills/` for platforms that support the shared layer. + +## Write To `.trellis/spec/` + +If the content is a coding convention, write it to spec. Examples: + +```text +.trellis/spec/backend/error-handling.md +.trellis/spec/frontend/components.md +.trellis/spec/guides/cross-platform-thinking-guide.md +``` + +After writing it, update the corresponding `index.md` so AI can find the new rule from the entry point. + +## Make The Current Task Use New Conventions + +After writing a spec, add it to the current task context: + +```bash +python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/backend/error-handling.md" "Error handling conventions" +python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/backend/error-handling.md" "Review error handling" +``` + +## Do Not Store Project-Private Rules In `trellis-meta` + +`trellis-meta` is a public skill for understanding Trellis architecture and local customization entry points. Put project-private content in: + +- `.trellis/spec/` +- a project-local skill +- the current task +- workspace journal + +This prevents future updates to Trellis's built-in `trellis-meta` from overwriting the team's own conventions. diff --git a/.claude/skills/trellis-meta/references/customize-local/change-agents.md b/.claude/skills/trellis-meta/references/customize-local/change-agents.md new file mode 100644 index 0000000..88e7376 --- /dev/null +++ b/.claude/skills/trellis-meta/references/customize-local/change-agents.md @@ -0,0 +1,56 @@ +# Change Local Agents + +When the user wants to change `trellis-research`, `trellis-implement`, or `trellis-check` behavior, edit platform agent files in the user project. + +## Read These Files First + +1. Target platform agent directory +2. `.trellis/workflow.md` Phase 2 / research routing +3. Current task `prd.md` +4. Current task `implement.jsonl` / `check.jsonl` +5. Relevant hook or agent prelude + +## Common Paths + +| Platform | Path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/cli/agents/trellis-*.md` | + +Use the actual paths in the user project as authoritative. + +## Common Needs + +| Need | Which agent to edit | +| --- | --- | +| Research must write files, not only reply in chat | `trellis-research` | +| Certain local specs must be read before implementation | `trellis-implement` + `implement.jsonl` configuration rules | +| Specific commands must run during checking | `trellis-check` | +| Agent must not modify certain directories | The corresponding agent's write boundary instructions | +| Agent output format must be fixed | The corresponding agent's final/reporting instructions | + +## Modification Principles + +1. **Preserve role boundaries**: research investigates and persists; implement writes implementation; check reviews and fixes. +2. **Do not hard-code project specs into agents**: long-term specs belong in `.trellis/spec/`; agents are responsible for reading them. +3. **Make read order explicit**: active task -> PRD -> info -> JSONL -> spec/research. +4. **Make write boundaries explicit**: which directories may be written and which may not. +5. **Synchronize across platforms**: when the user configured multiple platforms, decide whether to change only the current platform or all platform agents. + +## Agent Pull Platforms + +If an agent file contains a prelude for "read task/context after startup," do not remove those steps when editing. Otherwise the agent will work only from chat context and bypass Trellis's core mechanism. + +## Hook Push Platforms + +If context is injected by a hook, the agent file should still retain responsibility boundaries. Do not remove PRD/spec requirements from the agent just because a hook injects context. diff --git a/.claude/skills/trellis-meta/references/customize-local/change-context-loading.md b/.claude/skills/trellis-meta/references/customize-local/change-context-loading.md new file mode 100644 index 0000000..002a259 --- /dev/null +++ b/.claude/skills/trellis-meta/references/customize-local/change-context-loading.md @@ -0,0 +1,84 @@ +# Change Local Context Loading + +Context loading determines when AI reads workflow, task, spec, research, workspace, and git status. Read this page when the user says "AI does not know the current task," "the agent did not read specs," or "there is too much/too little context." + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/scripts/get_context.py` +3. `.trellis/scripts/common/session_context.py` +4. `.trellis/scripts/common/task_context.py` +5. `.trellis/scripts/common/active_task.py` +6. Current platform hooks or agent files +7. The current task's `implement.jsonl` / `check.jsonl` + +## Context Sources + +| Source | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow and next-action hints. | +| `.trellis/tasks/<task>/prd.md` | Current task requirements. | +| `.trellis/tasks/<task>/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. | +| `.trellis/workspace/` | Session records. | +| git status | Current working tree changes. | + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Inject more/less information in new sessions | `session_context.py` or the platform `session-start` hook. | +| Change hints on each user input | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The `inject-workflow-state` hook is parser-only and reads the block verbatim. | +| Agent did not read specs | Task JSONL, agent prelude, `inject-subagent-context` hook. | +| Active task is lost | `active_task.py` and platform session identity propagation. | +| Change JSONL validation rules | `task_context.py`. | + +## JSONL Rules + +`implement.jsonl` / `check.jsonl` are the key context loading interface: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-x/research/api.md", "reason": "API research"} +``` + +Include only spec/research files. Do not put code files that will be modified into these manifests; agents read code files themselves during implementation. + +## Change Session Context + +If the user wants every new session to see more project state, edit: + +- `.trellis/scripts/common/session_context.py` +- the corresponding platform `session-start` hook + +Context cannot grow without bound. Prefer injecting indexes and paths so the AI can read detailed files on demand. + +## Change Sub-Agent Context + +First determine which mode the platform uses: + +- hook push: edit the `inject-subagent-context` hook. +- agent pull: edit the read steps in the corresponding `trellis-implement` / `trellis-check` agent file. + +In both modes, make sure the agent ultimately reads: + +1. active task +2. 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 + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py list-context <task> +python3 ./.trellis/scripts/task.py validate <task> +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Confirm the task and JSONL are correct before editing hooks/agents. diff --git a/.claude/skills/trellis-meta/references/customize-local/change-hooks.md b/.claude/skills/trellis-meta/references/customize-local/change-hooks.md new file mode 100644 index 0000000..79aa5c5 --- /dev/null +++ b/.claude/skills/trellis-meta/references/customize-local/change-hooks.md @@ -0,0 +1,57 @@ +# Change Local Hooks + +Hooks are the automation layer that connects a platform to Trellis. When the user wants to change "when context is injected," "how shell commands inherit a session," or "which files are read before an agent starts," hooks are usually the edit point. + +## Read These Files First + +1. Target platform settings/config, such as `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`, `.trae/hooks.json` +2. Target platform hooks directory +3. `.trellis/scripts/common/active_task.py` +4. `.trellis/scripts/common/session_context.py` +5. `.trellis/workflow.md` + +## Common Hook Types + +| Hook | Purpose | +| --- | --- | +| session-start | Injects a Trellis overview when a session starts, clears, or compacts. | +| workflow-state | Injects a state hint on each user input. | +| sub-agent context | Injects PRD/spec/research before an agent starts. | +| shell session bridge | Lets `task.py` commands in shell see the same session identity. | + +## Modification Steps + +1. Find the hook registration in settings/config. +2. Confirm the registered script path exists. +3. Read the hook script and identify inputs, outputs, and called `.trellis/scripts/`. +4. Modify hook behavior. +5. If the hook depends on workflow content, synchronize `.trellis/workflow.md`. + +## Example: Change New-Session Injection Content + +First find the session-start hook: + +```text +.claude/settings.json +.claude/hooks/session-start.py +``` + +If the hook ultimately calls `.trellis/scripts/get_context.py` or `session_context.py`, editing the local script is usually more robust than hard-coding content in the hook. + +## Example: Agent Did Not Read JSONL + +First confirm: + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py validate <task> +``` + +If the task and JSONL are correct, determine whether the platform uses hook push or agent pull. For hook push, edit `inject-subagent-context`; for agent pull, edit the agent file. + +## Notes + +- Settings handle registration, hook scripts handle behavior; inspect both together. +- Different platforms support different hook events. Do not directly copy another platform's settings. +- Hooks should read project-local `.trellis/`; they should not depend on Trellis upstream source paths. +- Hook failures should produce visible errors so AI does not silently lose context. diff --git a/.claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md b/.claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md new file mode 100644 index 0000000..0ed8792 --- /dev/null +++ b/.claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md @@ -0,0 +1,123 @@ +# Change Local Skills, Commands, Prompts, And Workflows + +When the user wants to change AI entry points, auto-trigger rules, or explicit command behavior, edit skills, commands, prompts, or workflows in local platform directories. + +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 + +| Goal | Recommendation | +| --- | --- | +| AI should automatically know a capability | Add or modify a skill. | +| User wants to trigger manually with a command | Add or modify a command/prompt/workflow. | +| Team project conventions | Prefer `.trellis/spec/` or a project-local skill — 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 + +A skill is usually: + +```text +<skill-name>/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should be short and responsible for triggering/routing. Put long content in `references/` so AI can read it on demand. + +The frontmatter description should specify when to use the skill. Example: + +```yaml +description: "Use when customizing this project's deployment workflow and release checklist." +``` + +Do not write vague descriptions such as "helpful project skill"; they can trigger incorrectly. + +### 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: + +- How the user triggers it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +If a command only repeats workflow rules, prefer making it reference/read `.trellis/workflow.md` instead of maintaining a second copy of the flow. + +## Common Paths + +| Platform | Entry directories | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| 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/` | +| Factory Droid | `.factory/skills/`, `.factory/commands/` | +| Pi Agent | `.pi/skills/` | +| Reasonix | `.reasonix/skills/` (no separate commands dir; slash commands built into the platform) | +| ZCode | `.agents/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 — 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 (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/.claude/skills/trellis-meta/references/customize-local/change-spec-structure.md b/.claude/skills/trellis-meta/references/customize-local/change-spec-structure.md new file mode 100644 index 0000000..ee9a176 --- /dev/null +++ b/.claude/skills/trellis-meta/references/customize-local/change-spec-structure.md @@ -0,0 +1,83 @@ +# Change Local Spec Structure + +When the user wants to change the engineering conventions AI follows, add new spec layers, or adjust monorepo package mapping, edit `.trellis/spec/` and `.trellis/config.yaml`. + +## Read These Files First + +1. `.trellis/config.yaml` +2. `.trellis/spec/` +3. `.trellis/workflow.md` planning artifact guidance and Phase 3.3 +4. Current task `implement.jsonl` / `check.jsonl` + +## Common Needs + +| Need | Edit location | +| --- | --- | +| Add backend/frontend/docs/test spec layer | `.trellis/spec/<layer>/` or `.trellis/spec/<package>/<layer>/` | +| Add shared thinking guides | `.trellis/spec/guides/` | +| Adjust monorepo packages | `packages` in `.trellis/config.yaml` | +| Change default package | `default_package` in `.trellis/config.yaml` | +| Control spec scanning scope | `spec_scope` in `.trellis/config.yaml` | +| Make a task read a new spec | Task `implement.jsonl` / `check.jsonl` | + +## Add A Spec Layer + +Single-repository example: + +```text +.trellis/spec/security/ +├── index.md +└── auth.md +``` + +Monorepo example: + +```text +.trellis/spec/webapp/security/ +├── index.md +└── auth.md +``` + +`index.md` should include: + +- What code this layer applies to. +- Pre-Development Checklist. +- Quality Check. +- Links to specific guideline files. + +## Update Context + +Adding a spec does not mean every task automatically reads it. The current task must reference it in JSONL: + +```bash +python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/webapp/security/index.md" "Security conventions" +python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/webapp/security/index.md" "Security review rules" +``` + +## Change Monorepo Packages + +Example `.trellis/config.yaml`: + +```yaml +packages: + webapp: + path: apps/web + api: + path: apps/api +default_package: webapp +``` + +After editing, run: + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Use this output to confirm AI can see the correct packages and spec layers. + +## Notes + +- Specs are user project conventions and can be changed according to project needs. +- Do not put temporary task information into specs; put temporary information in the task. +- Do not put long-term conventions only in agents or commands; preserve them in specs. +- After changing spec structure, check whether existing task JSONL files still point to files that exist. diff --git a/.claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md b/.claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md new file mode 100644 index 0000000..a7a340f --- /dev/null +++ b/.claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md @@ -0,0 +1,90 @@ +# Change Local Task Lifecycle + +Task lifecycle includes creation, start, context configuration, finish, archive, parent/child tasks, and lifecycle hooks. The default customization targets are `.trellis/tasks/`, `.trellis/config.yaml`, and `.trellis/scripts/`. + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/config.yaml` +3. `.trellis/scripts/task.py` +4. `.trellis/scripts/common/task_store.py` +5. `.trellis/scripts/common/task_utils.py` +6. The current task's `.trellis/tasks/<task>/task.json` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Automatically sync an external system after task creation | `hooks.after_create` in `.trellis/config.yaml`. | +| Automatically update status after task start | `hooks.after_start` in `.trellis/config.yaml`. | +| Run a script after task finish | `hooks.after_finish` in `.trellis/config.yaml`. | +| Clean external resources after archive | `hooks.after_archive` in `.trellis/config.yaml`. | +| Change default task fields | `.trellis/scripts/common/task_store.py`. | +| Change task parsing/search | `.trellis/scripts/common/task_utils.py`. | +| Change active task behavior | `.trellis/scripts/common/active_task.py`. | + +## lifecycle hooks + +`.trellis/config.yaml` supports: + +```yaml +hooks: + after_create: + - "python3 .trellis/scripts/hooks/my_sync.py create" + after_start: + - "python3 .trellis/scripts/hooks/my_sync.py start" + after_finish: + - "python3 .trellis/scripts/hooks/my_sync.py finish" + after_archive: + - "python3 .trellis/scripts/hooks/my_sync.py archive" +``` + +Hook commands receive the `TASK_JSON_PATH` environment variable, pointing to the current task's `task.json`. Hook failures should usually warn, but not block the main task operation. + +## Change Task Fields + +If the user wants to add project-local fields, prefer putting them under `meta` in `task.json` to avoid breaking existing scripts' assumptions about standard fields. + +Example: + +```json +"meta": { + "linearIssue": "ENG-123", + "risk": "high" +} +``` + +If standard fields really need to change, inspect every local script that reads `task.json`. + +## Change Active Task + +Active task is session-level state stored in `.trellis/.runtime/sessions/`. Do not fall back to a global `.current-task` model. If the user wants to change active task behavior, edit: + +- `.trellis/scripts/common/active_task.py` +- platform hooks or shell session bridges +- active task descriptions in `.trellis/workflow.md` + +### `task.py create` Sets the Active Pointer + +`cmd_create` in `.trellis/scripts/common/task_store.py` calls `set_active_task` best-effort right after writing the new task directory. The behavior: + +- When the calling shell carries session identity (`TRELLIS_CONTEXT_ID` env var, or any platform-specific session env that `resolve_context_key` recognizes — see `active_task.py:_ENV_SESSION_KEYS`), the per-session pointer at `.trellis/.runtime/sessions/<context_key>.json` is rewritten to point at the new task. The task's `status=planning` and `[workflow-state:planning]` fires on the very next `UserPromptSubmit`. +- When session identity is unavailable (raw CLI invocation outside an AI session, or a platform that doesn't propagate identity to shell), the task directory is still created and `status=planning` is still written, but the active pointer is left untouched. The user can attach the task later with `task.py start <dir>` once they're back in an AI session. + +This makes `[workflow-state:planning]` the live breadcrumb during the brainstorm and JSONL curation work that follows `task.py create`. The pre-R7 behavior left the breadcrumb stuck on `no_task` until `task.py start`, so the planning block was effectively dead text. + +If you fork `task.py` to add a new creation path (e.g. an external import that bypasses `cmd_create`), audit whether your path also calls `set_active_task`. Without that call, your created tasks will not surface as active. The full status writer table is in `.trellis/spec/cli/backend/workflow-state-contract.md`. + +## Modification Steps + +1. Confirm the current task with `python3 ./.trellis/scripts/task.py current --source`. +2. Read the current task's `task.json` and confirm status and fields. +3. For configuration needs, edit `.trellis/config.yaml` first. +4. For script behavior needs, then edit `.trellis/scripts/`. +5. If the AI flow changed, synchronize `.trellis/workflow.md`. + +## Do Not + +- Do not directly edit `.trellis/.runtime/sessions/` to "fix" business state. +- Do not hard-code project-private fields into scripts; prefer `meta`. +- Do not default to asking the user to fork Trellis CLI. diff --git a/.claude/skills/trellis-meta/references/customize-local/change-workflow.md b/.claude/skills/trellis-meta/references/customize-local/change-workflow.md new file mode 100644 index 0000000..337c985 --- /dev/null +++ b/.claude/skills/trellis-meta/references/customize-local/change-workflow.md @@ -0,0 +1,65 @@ +# Change Local Workflow + +When the user wants to change Trellis phases, next-action hints, whether to create tasks, whether to use sub-agents, or when to check/wrap up, edit `.trellis/workflow.md` first. + +## Read These Files First + +1. `.trellis/workflow.md` +2. Entry files for the current platform, such as skills/commands/prompts/workflows +3. The current task's `task.json` and `prd.md` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Change phase names or phase order | `Phase Index` and the corresponding Phase sections. | +| Change whether to create a task when there is no task | `[workflow-state:no_task]` state block. | +| Change the next step during planning | Phase 1 and `[workflow-state:planning]`. | +| Change whether an agent is required during in_progress | Phase 2 and `[workflow-state:in_progress]`. | +| Change wrap-up after completion | Phase 3 and `[workflow-state:completed]`. | +| Change which skill a user intent triggers | `Skill Routing` table. | + +## Modification Steps + +1. Find the relevant section in `.trellis/workflow.md`. +2. When changing rules, keep explicit trigger conditions and next actions. +3. If adding or renaming a skill/agent, synchronize the corresponding files in platform directories. +4. Workflow-state changes only need an edit to the `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook is parser-only — it reads whatever you put in the block. Keep the opening and closing tags' STATUS strings identical (`[workflow-state:foo]…[/workflow-state:foo]`); mismatched STATUS pairs are silently dropped. +5. Make the AI reread `.trellis/workflow.md`; do not keep using rules from the old conversation. + +## Example: Relax Task Creation Requirements + +To change when task creation can be skipped, usually edit `[workflow-state:no_task]`: + +```md +[workflow-state:no_task] +Task is not required when the answer is a one-reply explanation, no files are changed, and no research is needed. +[/workflow-state:no_task] +``` + +If the formal Phase 1 flow also needs to change, synchronize the Phase 1 section. + +## Example: One Platform Does Not Use Sub-Agents + +If the user wants only one platform to avoid sub-agents, first confirm whether that platform has a separate group in the workflow. Then change Phase 2 routing for that platform group instead of deleting all `trellis-implement` / `trellis-check` instructions across platforms. + +## `/trellis:continue` Route Table + +`/trellis:continue` resumes a task by deciding which phase step to load next. The decision combines `task.json.status` with the presence of artifacts inside the task directory. The mapping is fixed in the command itself; forks that add custom statuses must extend both the workflow.md tag block and this table. + +| `status` | Artifact state | Resume at | +| --- | --- | --- | +| `planning` | `prd.md` missing | Phase 1.1 (load `trellis-brainstorm`) | +| `planning` | 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.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. + +## Notes + +`.trellis/workflow.md` is the local project workflow, not an immutable template. The user can adapt it to team habits. After editing it, platform entry files may still contain old descriptions, so inspect them too. diff --git a/.claude/skills/trellis-meta/references/customize-local/overview.md b/.claude/skills/trellis-meta/references/customize-local/overview.md new file mode 100644 index 0000000..b75d208 --- /dev/null +++ b/.claude/skills/trellis-meta/references/customize-local/overview.md @@ -0,0 +1,55 @@ +# Local Customization Overview + +This directory is for local AI working in a user project where Trellis was installed through npm and `trellis init` has already been run. The AI should modify generated `.trellis/` and platform directories inside the project, not Trellis CLI upstream source code. + +## First Determine What The User Actually Wants To Change + +| User wording | Read first | +| --- | --- | +| "Change the Trellis flow / phases / next prompt" | `change-workflow.md` | +| "Change task creation, status, archive, or hooks" | `change-task-lifecycle.md` | +| "AI did not read context / change injected content" | `change-context-loading.md` | +| "A platform hook is not behaving as expected" | `change-hooks.md` | +| "Change implement/check/research agent behavior" | `change-agents.md` | +| "Add a skill/command/workflow/prompt" | `change-skills-or-commands.md` | +| "Adjust the project spec structure" | `change-spec-structure.md` | +| "Add team conventions and local notes" | `add-project-local-conventions.md` | + +## General Operation Order + +1. **Confirm platform and directories**: inspect which directories exist, such as `.claude/`, `.codex/`, `.cursor/`, `.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. +5. **Synchronize semantics**: if a shared flow changes, check whether platform entry points also need changes; if a platform entry changes, check whether `.trellis/workflow.md` still agrees. + +## Local File Priority + +| Layer | Files | +| --- | --- | +| Workflow | `.trellis/workflow.md` | +| Project configuration | `.trellis/config.yaml` | +| Task material | `.trellis/tasks/<task>/` | +| Project specs | `.trellis/spec/` | +| Runtime scripts | `.trellis/scripts/` | +| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.zcode/`, and similar directories | +| Shared skill | `.agents/skills/` | + +## Things Not To Do By Default + +- Do not edit the global npm install directory. +- Do not edit `node_modules/@mindfoldhq/trellis`. +- Do not assume the user has the Trellis GitHub repository. +- Do not overwrite local files already modified by the user with default templates. +- Do not put team project rules into public `trellis-meta`; project rules belong in `.trellis/spec/` or a local skill. + +## When To Inspect Upstream Source + +Switch to an upstream source-code perspective only when the user explicitly expresses one of these goals: + +- "I want to open a PR to Trellis" +- "I want to change npm package publish contents" +- "I want to fork Trellis" +- "I want to modify the generation logic for `trellis init/update`" + +Otherwise, default to modifying local Trellis files inside the user project. diff --git a/.claude/skills/trellis-meta/references/local-architecture/bundled-skills.md b/.claude/skills/trellis-meta/references/local-architecture/bundled-skills.md new file mode 100644 index 0000000..1c819cb --- /dev/null +++ b/.claude/skills/trellis-meta/references/local-architecture/bundled-skills.md @@ -0,0 +1,146 @@ +# 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` | +| 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`, 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/.claude/skills/trellis-meta/references/local-architecture/context-injection.md b/.claude/skills/trellis-meta/references/local-architecture/context-injection.md new file mode 100644 index 0000000..4a7517b --- /dev/null +++ b/.claude/skills/trellis-meta/references/local-architecture/context-injection.md @@ -0,0 +1,68 @@ +# Local Context Injection System + +Trellis context injection aims to make AI read the right files at the right time instead of relying on model memory. In a user project, injection is implemented by `.trellis/` scripts together with platform hooks, agents, and skills. + +## Injected Context Types + +| Type | Source | Purpose | +| --- | --- | --- | +| session context | `.trellis/scripts/get_context.py` | Current developer, git status, active task, active tasks, journal, packages. | +| workflow context | `.trellis/workflow.md` | Current Trellis flow and next action. | +| spec context | `.trellis/spec/` + task JSONL | Specs that must be followed during implementation/checking. | +| task context | `.trellis/tasks/<task>/prd.md`, `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 + +Platforms with session-start support inject a Trellis overview when a session starts, clears, compacts, or receives a similar event. Injected content usually includes: + +- workflow summary. +- current task status. +- active tasks. +- spec index paths. +- developer identity and git status. + +If the user feels the AI does not know the current task in a new session, first check whether the platform's session-start hook or equivalent mechanism is installed and running. + +## workflow-state + +workflow-state is a lightweight hint injected around each user turn. Based on current task status, it selects a block from `.trellis/workflow.md`, such as `no_task`, `planning`, `in_progress`, or `completed`. + +If the user wants to change "what the AI should do next in a given state," edit the corresponding state block in `.trellis/workflow.md` first. + +## sub-agent context + +Implement and check agents need task context. Trellis has two loading modes: + +1. **hook push**: a platform hook injects 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 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 + +`implement.jsonl` and `check.jsonl` contain one JSON object per line: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend rules"} +``` + +Readers should skip seed rows without a `file` field. When configuring JSONL, the AI should include only spec/research files, not pre-register code files that will be modified. + +## Active Task And Context Key + +Active task state lives in `.trellis/.runtime/sessions/` and is isolated per session. Hooks try to resolve the context key from platform events, environment variables, transcript paths, or `TRELLIS_CONTEXT_ID`. + +If shell commands cannot see the same context key, `task.py current --source` may report no active task. In that case, check whether the platform passes session identity into the shell instead of hand-writing a global current-task file. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change session-start injected content | The platform's `session-start` hook or plugin file. | +| Change per-turn workflow-state rules | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The platform workflow-state hook parses these blocks verbatim and embeds no fallback text. | +| Change how sub-agents read context | Platform agent definitions, the `inject-subagent-context` hook, or agent preludes. | +| Change JSONL validation/display | `.trellis/scripts/common/task_context.py`. | +| Change active task resolution | `.trellis/scripts/common/active_task.py`. | + +When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct task artifacts/spec/research. diff --git a/.claude/skills/trellis-meta/references/local-architecture/generated-files.md b/.claude/skills/trellis-meta/references/local-architecture/generated-files.md new file mode 100644 index 0000000..5e53865 --- /dev/null +++ b/.claude/skills/trellis-meta/references/local-architecture/generated-files.md @@ -0,0 +1,80 @@ +# Local Files Generated After Init + +`trellis init` writes the Trellis runtime into the user project. Later, `trellis update` tries to update Trellis-managed template files, but it uses `.trellis/.template-hashes.json` to determine which files have already been modified by the user. + +This page only describes files that are visible and editable inside the user project. + +## `.trellis/` + +```text +.trellis/ +├── workflow.md +├── config.yaml +├── .developer +├── .version +├── .template-hashes.json +├── .runtime/ +├── scripts/ +├── spec/ +├── tasks/ +└── workspace/ +``` + +| Path | Usually editable? | Notes | +| --- | --- | --- | +| `.trellis/workflow.md` | Yes | Local workflow documentation and AI routing rules. | +| `.trellis/config.yaml` | Yes | Project configuration, hooks, packages, journal line limits, and related settings. | +| `.trellis/spec/` | Yes | Project specs, intended to be updated regularly by users and AI. | +| `.trellis/tasks/` | Yes | Task material and research artifacts, maintained by the task workflow. | +| `.trellis/workspace/` | Yes | Session records, usually written by `add_session.py`. | +| `.trellis/scripts/` | Carefully | Local runtime. It can be customized, but only after understanding the call chain. | +| `.trellis/.runtime/` | No | Runtime state, usually written automatically by hooks/scripts. | +| `.trellis/.developer` | Carefully | Current developer identity. | +| `.trellis/.version` | No | Trellis version record used by update/migration logic. | +| `.trellis/.template-hashes.json` | No | Template hash record. Do not hand-write business rules here. | + +## Platform Directories + +Different platforms generate different directories. Common categories: + +| Category | Example paths | Purpose | +| --- | --- | --- | +| hooks | `.claude/hooks/`, `.codex/hooks/`, `.cursor/hooks/` | Inject session context, workflow-state, and sub-agent context. | +| settings | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.trae/hooks.json` | Tell the platform when to run hooks or plugins. | +| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/`, `.zcode/cli/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/`, `.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. + +## Meaning Of Template Hashes + +`.trellis/.template-hashes.json` records the content hash from the last time Trellis wrote a template file. `trellis update` uses it to distinguish three cases: + +| Case | Update behavior | +| --- | --- | +| File was not modified by the user | It can be updated automatically. | +| File was modified by the user | Prompt the user to overwrite, keep, or generate `.new`. | +| File is no longer a current template | It may be deleted, renamed, or preserved according to migration rules. | + +When an AI customizes local Trellis files, it does not need to maintain hashes manually. It is normal for Trellis update to recognize the result as "modified by the user." + +## Local Customization Boundaries + +Editable by default: + +- `.trellis/workflow.md` +- `.trellis/config.yaml` +- `.trellis/spec/**` +- `.trellis/scripts/**` +- Platform hooks, settings, agents, skills, commands, prompts, and workflows + +Do not edit by default: + +- Global npm install directory +- `node_modules/@mindfoldhq/trellis` +- Trellis GitHub repository source code +- Concrete state files under `.trellis/.runtime/**` +- Hash contents inside `.trellis/.template-hashes.json` + +Switch to the Trellis CLI source-code perspective only when the user explicitly wants to contribute upstream. diff --git a/.claude/skills/trellis-meta/references/local-architecture/multi-agent-channel.md b/.claude/skills/trellis-meta/references/local-architecture/multi-agent-channel.md new file mode 100644 index 0000000..6df61eb --- /dev/null +++ b/.claude/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/.claude/skills/trellis-meta/references/local-architecture/overview.md b/.claude/skills/trellis-meta/references/local-architecture/overview.md new file mode 100644 index 0000000..e97cab8 --- /dev/null +++ b/.claude/skills/trellis-meta/references/local-architecture/overview.md @@ -0,0 +1,51 @@ +# Local Trellis Architecture Overview + +`trellis-meta` is for user projects that have already run `trellis init`. The user's machine usually has only the npm-installed `trellis` command plus the Trellis files generated inside the project; it may not have the Trellis CLI source code. + +Therefore, when an AI uses this skill, the default customization target is local files inside the user project: + +- `.trellis/`: workflow, tasks, specs, memory, scripts, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.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. + +## Local System Model + +Trellis provides three layers inside a user project: + +1. **Workflow layer**: `.trellis/workflow.md` defines phases, routing, next actions, and prompt blocks. +2. **Persistence layer**: `.trellis/tasks/`, `.trellis/spec/`, and `.trellis/workspace/` store tasks, specs, and session memory. +3. **Platform integration layer**: hooks, settings, agents, skills, commands, prompts, and workflows in platform directories connect the Trellis workflow to different AI tools. + +All three layers live inside the user project, so an AI can read and modify them directly. + +## Core Paths + +| Path | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow phases, skill routing, and workflow-state prompt blocks. | +| `.trellis/config.yaml` | Project configuration, task lifecycle hooks, monorepo package configuration, and journal configuration. | +| `.trellis/spec/` | The user's project-specific coding conventions and thinking guides. | +| `.trellis/tasks/` | Each task's PRD, technical notes, research files, and JSONL context. | +| `.trellis/workspace/` | Per-developer journals and cross-session memory. | +| `.trellis/scripts/` | Local Python runtime used by commands, hooks, and context injection. | +| `.trellis/.runtime/` | Session-level runtime state, such as the current task pointer. | +| `.trellis/.template-hashes.json` | Template hashes for Trellis-managed files, used by update to determine whether local files were modified by the user. | + +## AI Customization Principles + +1. **Find the local source of truth first**: Do not edit from memory. Read `.trellis/workflow.md`, `.trellis/config.yaml`, the relevant platform directory, and related task files first. +2. **Edit the user project, not the npm package cache**: Modify generated files inside the project, not `node_modules` or the global npm install directory. +3. **Keep platform files aligned with `.trellis/`**: If workflow routing changes, also check whether platform skills or commands still describe the same flow. +4. **Put project-specific rules in `.trellis/spec/` or a local skill**: Do not put team conventions into `trellis-meta`. +5. **Preserve user changes**: If a file was already modified locally, work from the current content instead of overwriting it with a default template. + +## How To Use This Directory + +- To understand which files exist after init, read `generated-files.md`. +- To change phases, routing, or next actions, read `workflow.md`. +- To change the task model, JSONL context, or active task behavior, read `task-system.md`. +- To change coding convention injection, read `spec-system.md`. +- To understand journals and cross-session memory, read `workspace-memory.md`. +- To change hooks or sub-agent context loading, read `context-injection.md`. diff --git a/.claude/skills/trellis-meta/references/local-architecture/spec-system.md b/.claude/skills/trellis-meta/references/local-architecture/spec-system.md new file mode 100644 index 0000000..38fdf14 --- /dev/null +++ b/.claude/skills/trellis-meta/references/local-architecture/spec-system.md @@ -0,0 +1,102 @@ +# Local Spec System + +`.trellis/spec/` is the user's project-specific engineering spec library. Trellis is not about making AI memorize conventions; it injects relevant specs or requires the AI to read them at the right time. + +## Directory Model + +A common single-repository structure: + +```text +.trellis/spec/ +├── backend/ +│ ├── index.md +│ └── ... +├── frontend/ +│ ├── index.md +│ └── ... +└── guides/ + ├── index.md + └── ... +``` + +A common monorepo structure: + +```text +.trellis/spec/ +├── cli/ +│ ├── backend/ +│ │ ├── index.md +│ │ └── ... +│ └── unit-test/ +│ ├── index.md +│ └── ... +├── docs-site/ +│ └── docs/ +│ ├── index.md +│ └── ... +└── guides/ + ├── index.md + └── ... +``` + +`index.md` is the entry point for each layer. It should list the Pre-Development Checklist and Quality Check. Specific guidelines live in other Markdown files in the same directory. + +## Package Configuration + +`.trellis/config.yaml` can declare packages: + +```yaml +packages: + cli: + path: packages/cli + docs-site: + path: docs-site + type: submodule +default_package: cli +``` + +The AI can run: + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +This command lists packages and spec layers for the current project. Use this output as the reference when configuring context JSONL. + +## How Specs Enter Tasks + +Before a task enters implementation, 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"} +{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Test expectations"} +``` + +Sub-agents or platform preludes read these JSONL files and load the referenced specs. On platforms without sub-agent support, the AI should read the relevant specs directly according to the workflow. + +## What Specs Should Contain + +Specs should contain executable engineering conventions for the project, not generic best practices: + +- Where files should live. +- How error handling should be expressed. +- Input/output contracts for APIs, hooks, and commands. +- Patterns that are forbidden. +- Cases that require tests. +- Project-specific pitfalls and how to avoid them. + +When the AI learns a new rule during implementation or debugging, it should update `.trellis/spec/` rather than only summarizing it in chat. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Add a new spec layer | `.trellis/spec/<package>/<layer>/index.md` and corresponding guideline files. | +| Change monorepo spec mapping | `packages` / `default_package` / `spec_scope` in `.trellis/config.yaml`. | +| Change which specs AI reads before implementation | The task's `implement.jsonl`. | +| Change which specs AI reads during checking | The task's `check.jsonl`. | +| Change when specs should be updated | Phase 3.3 in `.trellis/workflow.md` and the `trellis-update-spec` skill. | + +## Boundaries + +`.trellis/spec/` is the user's project specification, not a permanent copy of Trellis built-in templates. The AI should encourage the user to update it according to the actual project code instead of treating Trellis default templates as immutable documents. diff --git a/.claude/skills/trellis-meta/references/local-architecture/task-system.md b/.claude/skills/trellis-meta/references/local-architecture/task-system.md new file mode 100644 index 0000000..7133495 --- /dev/null +++ b/.claude/skills/trellis-meta/references/local-architecture/task-system.md @@ -0,0 +1,130 @@ +# Local Task System + +The Trellis task system is stored entirely under `.trellis/tasks/` in the user project. Each task is a directory containing requirements, context, research, state, and relationship information. + +## Task Directory Structure + +```text +.trellis/tasks/ +├── 04-28-example-task/ +│ ├── task.json +│ ├── prd.md +│ ├── design.md +│ ├── implement.md +│ ├── implement.jsonl +│ ├── check.jsonl +│ └── research/ +└── archive/ + └── 2026-04/ +``` + +| File | Purpose | +| --- | --- | +| `task.json` | Task metadata: status, assignee, priority, branch, parent/child tasks, and similar fields. | +| `prd.md` | Requirements, 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. | + +## `task.json` + +`task.json` records task status and metadata. Common fields: + +| Field | Meaning | +| --- | --- | +| `id` / `name` / `title` | Task identity and title. | +| `status` | Status such as `planning`, `in_progress`, `review`, or `completed`. | +| `priority` | `P0`, `P1`, `P2`, `P3`. | +| `creator` / `assignee` | Creator and assignee. | +| `package` | Target package in a monorepo; may be empty. | +| `branch` / `base_branch` | Working branch and PR target branch. | +| `children` / `parent` | Parent/child task relationships. | +| `commit` / `pr_url` | Commit and PR information after completion. | +| `meta` | Extension fields. | + +## 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 + +The user sees a "current task," but Trellis stores active task state per session. + +```text +.trellis/.runtime/sessions/<context-key>.json +``` + +`task.py start` writes the task path into the runtime session file for the current session. `task.py current --source` shows the current task and where it came from. Different AI windows can point to different tasks without overwriting each other. + +If the platform or shell environment has no stable session identity, `task.py start` may be unable to set the active task. The AI should read the error, inspect the platform hook/session environment, and not fall back to a shared global pointer. + +## JSONL Context + +`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first. They do not replace `implement.md`; `implement.md` is the human-readable execution plan. + +Format: + +```jsonl +{"file": ".trellis/spec/cli/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-example/research/api.md", "reason": "API research"} +``` + +Rules: + +- Include spec and research files. +- Do not include code files that are about to be modified. +- Do not treat temporary conclusions in chat as the only context. +- Seed rows have no `file` field; they only prompt the AI to fill in real entries. + +## Common Commands + +```bash +python3 ./.trellis/scripts/task.py create "<title>" --slug <slug> +python3 ./.trellis/scripts/task.py start <task> +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py add-context <task> implement <file> <reason> +python3 ./.trellis/scripts/task.py validate <task> +python3 ./.trellis/scripts/task.py finish +python3 ./.trellis/scripts/task.py archive <task> +``` + +When modifying the task system, the AI should prefer script commands to maintain structure. Edit JSON/Markdown directly only when scripts do not cover the need. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change the default task template | `.trellis/scripts/common/task_store.py` and task creation instructions. | +| Change status semantics | `.trellis/workflow.md`, workflow-state hook logic, and task usage conventions. | +| Add task lifecycle actions | `hooks.after_*` in `.trellis/config.yaml`. | +| Change context rules | 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/.claude/skills/trellis-meta/references/local-architecture/workflow.md b/.claude/skills/trellis-meta/references/local-architecture/workflow.md new file mode 100644 index 0000000..f0659ff --- /dev/null +++ b/.claude/skills/trellis-meta/references/local-architecture/workflow.md @@ -0,0 +1,75 @@ +# Local Workflow System + +`.trellis/workflow.md` is the Trellis workflow source of truth inside the user project. An AI does not need Trellis source code to understand how the current project should move tasks forward; this file is enough. + +## File Responsibilities + +`.trellis/workflow.md` has three responsibilities: + +1. **Explain workflow phases**: Plan, Execute, Finish. +2. **Define skill routing**: which skill or agent the AI should use when the user expresses a certain intent. +3. **Provide workflow-state prompt blocks**: hooks can inject the prompt block for the current state into the conversation. + +## Current Phase Model + +```text +Phase 1: Plan -> clarify what to build, produce prd.md and required research +Phase 2: Execute -> implement against the PRD and specs, then check +Phase 3: Finish -> final verification, preserve lessons, and wrap up +``` + +Each phase contains numbered steps, such as `1.3 Configure context`. These numbers are not runtime fields in `task.json`; they are workflow structure for AI and humans to read. + +## Skill Routing + +`workflow.md` separates routing by platform capability: + +- Platforms with sub-agent support: dispatch `trellis-implement` by default for implementation and `trellis-check` for checking. +- Platforms without sub-agent support: the main session reads skills such as `trellis-before-dev`, then executes directly. + +When changing local AI behavior, update the routing descriptions in `workflow.md` first, then check whether the corresponding platform skill, command, or agent files need to stay in sync. + +## Workflow-State Prompt Blocks + +The bottom of `workflow.md` can contain state blocks like this: + +```text +[workflow-state:no_task] +... +[/workflow-state:no_task] +``` + +Hooks choose the right block based on current task status and inject it into the conversation. Common states include: + +| State | Meaning | +| --- | --- | +| `no_task` | The current session has no active task. | +| `planning` | The task is still in requirements, research, or context configuration. | +| `in_progress` | The task has entered implementation and checking. | +| `completed` | The task is complete and waiting for wrap-up or archive. | + +If the user wants to change policies such as "whether to create a task when there is no task," "when task creation may be skipped," or "whether sub-agents are required," edit these state blocks and the routing table above them. + +## Local Modification Patterns + +Common changes: + +| Goal | Edit point | +| --- | --- | +| Add a phase | Update the Phase Index, phase body, routing, and state blocks. | +| Change task creation policy | Update the `no_task` state block and Phase 1 description. | +| Change the default implementation/check path | Update Phase 2 and skill routing. | +| Change the wrap-up flow | Update Phase 3 and `finish-work` related descriptions. Note the current split: Phase 3.4 = AI-driven code commits (batched, user-confirmed), Phase 3.5 = `/finish-work` (archive + record session). `/finish-work` refuses to run if the working tree is dirty. | +| Change platform differences | Update routing descriptions grouped by platform. | + +After editing, make the AI reread `.trellis/workflow.md`; do not assume the flow from the old conversation is still valid. + +## Relationship To Platform Files + +`workflow.md` is the semantic center of the local workflow, but each platform can also have its own entry files: + +- skills, such as `trellis-brainstorm` and `trellis-check`. +- commands/prompts/workflows, such as continue and finish-work. +- hooks, such as session-start or workflow-state injection. + +If only `workflow.md` changes, platform entry files may still contain old language. When the user wants to change "what the AI actually does," also inspect the relevant platform directory. diff --git a/.claude/skills/trellis-meta/references/local-architecture/workspace-memory.md b/.claude/skills/trellis-meta/references/local-architecture/workspace-memory.md new file mode 100644 index 0000000..c2958f2 --- /dev/null +++ b/.claude/skills/trellis-meta/references/local-architecture/workspace-memory.md @@ -0,0 +1,71 @@ +# Local Workspace Memory System + +`.trellis/workspace/` stores cross-session memory. Its purpose is to let AI and humans understand what happened before across different windows and different days. + +## Directory Structure + +```text +.trellis/workspace/ +├── index.md +└── <developer>/ + ├── index.md + ├── journal-1.md + └── journal-2.md +``` + +| File | Purpose | +| --- | --- | +| `.trellis/.developer` | Current developer identity. | +| `.trellis/workspace/index.md` | Global workspace overview. | +| `.trellis/workspace/<developer>/index.md` | Session index for a developer. | +| `.trellis/workspace/<developer>/journal-N.md` | Session journal. | + +## Developer Identity + +Run this the first time: + +```bash +python3 ./.trellis/scripts/init_developer.py <name> +``` + +This creates `.trellis/.developer` and the corresponding workspace directory. The AI should not change developer identity casually; if the identity is wrong, first confirm who is using the current project. + +## Journal + +`journal-N.md` records completed or partially completed work from each session. By default, each journal holds about 2000 lines; after that it rotates to the next file. + +Common command for recording a session: + +```bash +python3 ./.trellis/scripts/add_session.py \ + --title "Session title" \ + --summary "What changed" \ + --commit "abc1234" +``` + +Planning or review work without a commit can also be recorded by using `--no-commit` or an empty commit value. + +## Relationship Between Workspace Memory And Tasks + +| System | What it stores | +| --- | --- | +| `.trellis/tasks/` | Requirements, design, research, and state for a specific task. | +| `.trellis/workspace/` | Work records across tasks and sessions. | +| `.trellis/spec/` | Engineering knowledge preserved as long-term conventions. | + +If information is only useful for the current task, put it in the task directory. +If information describes what happened in the current session, put it in the workspace journal. +If information should be followed every time code is written in the future, put it in spec. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change maximum journal lines | `max_journal_lines` in `.trellis/config.yaml`. | +| Change session auto-commit message | `session_commit_message` in `.trellis/config.yaml`. | +| Change session content format | `.trellis/scripts/add_session.py`. | +| Change how workspace is displayed in context | `.trellis/scripts/common/session_context.py`. | + +## AI Usage Rules + +The AI should not treat workspace as the only source of truth. When resuming a task, read the current task first, then use workspace for background. After a task is complete, record important process notes in workspace; if long-term rules emerged, update spec. diff --git a/.claude/skills/trellis-meta/references/platform-files/agents.md b/.claude/skills/trellis-meta/references/platform-files/agents.md new file mode 100644 index 0000000..a07cae7 --- /dev/null +++ b/.claude/skills/trellis-meta/references/platform-files/agents.md @@ -0,0 +1,82 @@ +# Agents + +Trellis agent files define specialized roles. Common Trellis agents in a user project are: + +- `trellis-research` +- `trellis-implement` +- `trellis-check` + +File locations and formats differ by platform, but responsibility boundaries should stay consistent. + +## Agent Responsibilities + +| Agent | Responsibility | +| --- | --- | +| `trellis-research` | Investigate the question and write findings into the current task's `research/`. | +| `trellis-implement` | Implement against `prd.md`, 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. + +## Common Paths + +| Platform | Agent path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/cli/agents/trellis-*.md` | + +GitHub Copilot agent/prompt support is provided by a combination of directories such as `.github/agents/`, `.github/prompts/`, and `.github/skills/`; inspect the files actually generated in the user project. + +Main-session workflow platforms such as Kilo, Antigravity, and Devin may not have Trellis sub-agent files. They usually rely on workflows/skills to guide the main session. + +## Two Context Loading Modes + +### hook push + +The platform hook injects task context before the agent starts. The agent file itself can focus more on responsibilities and boundaries. + +Common on platforms that support agent hooks. + +### agent pull + +The agent file instructs the agent to read after startup: + +- `python3 ./.trellis/scripts/task.py current --source` +- `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. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Implement agent must follow extra restrictions | The platform's `trellis-implement` agent file. | +| Check agent must run project-specific commands | `trellis-check` agent file, and `.trellis/spec/` if needed. | +| Research agent must output a fixed format | `trellis-research` agent file. | +| Agent cannot read task context | Agent prelude or `inject-subagent-context` hook. | +| Add a project-specific agent | Platform agent directory + related workflow/command/skill entry point. | + +## Modification Principles + +1. **Keep responsibilities single-purpose**. Do not mix research, implement, and check responsibilities into one agent. +2. **Specify the read order**. Agents must know to start from the active task, 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. + +## Do Not Default To Editing Upstream Templates + +Local AI should default to modifying platform agent files inside the user project. Discuss upstream template source only when the user explicitly wants to contribute the change back to Trellis. diff --git a/.claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md b/.claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md new file mode 100644 index 0000000..d4ae8f6 --- /dev/null +++ b/.claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md @@ -0,0 +1,72 @@ +# Hooks And Settings + +Hooks/settings are the entry layer that connects a platform to Trellis. They decide which scripts, plugins, or extensions a platform runs for which events. + +## Settings Responsibilities + +settings/config files usually register: + +- session-start hook: injects a Trellis overview when a new session starts or context resets. +- workflow-state hook: parses `[workflow-state:STATUS]` blocks from `.trellis/workflow.md` and emits the body matching the current task `status` on each user input. Parser-only; the script does not embed fallback content. +- sub-agent context hook: injects task context when implementation/check/research agents start. +- shell/session bridge: lets shell commands see the same Trellis session identity. +- platform plugin or extension entry points. + +Common files: + +| Platform | settings/config | +| --- | --- | +| Claude Code | `.claude/settings.json` | +| Cursor | `.cursor/hooks.json` | +| Codex | `.codex/hooks.json`, `.codex/config.toml` | +| OpenCode | `.opencode/package.json`, `.opencode/plugins/*` | +| Kiro | `.kiro/hooks/` + platform config | +| Gemini CLI | `.gemini/settings.json` | +| Qoder | `.qoder/settings.json` | +| CodeBuddy | `.codebuddy/settings.json` | +| GitHub Copilot | `.github/copilot/hooks.json` | +| Factory Droid | `.factory/settings.json` | +| Pi Agent | `.pi/settings.json`, `.pi/extensions/trellis/` | +| Trae IDE | `.trae/hooks.json` | + +Reasonix and ZCode are pull-based platforms that do not use hooks or settings files; their agent files contain prelude instructions to read context after startup. + +Whether these files exist in a project depends on which `trellis init --<platform>` flags the user ran. + +## Hook Script Types + +| Script | Purpose | +| --- | --- | +| `session-start.py` | Generates session-start context. | +| `inject-workflow-state.py` | Parses `[workflow-state:STATUS]` blocks in `.trellis/workflow.md` and emits the body matching the current task status. Falls back to `Refer to workflow.md for current step.` when no matching block exists. | +| `inject-subagent-context.py` | Injects PRD, JSONL context, and related spec/research into sub-agents. | +| `inject-shell-session-context.py` | Lets shell commands inherit Trellis session identity. | + +Not every platform has every hook. Do not copy files from another platform just because a platform lacks a hook; first confirm whether that platform supports the corresponding event. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| AI should see more/less context in a new session | Platform `session-start` hook. | +| Per-turn hint policy should change | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook parses workflow.md verbatim — no script edit required. | +| Sub-agent cannot read PRD/spec | `inject-subagent-context` hook or agent prelude. | +| `task.py current` in shell has no active task | Shell/session bridge hook or platform environment variable configuration. | +| Disable an automatic injection | The corresponding hook registration in settings/config. | + +## Modification Principles + +1. **Settings wire things up; hooks define behavior**. If only the hook changes, the platform may never call it. If only settings change, behavior may not change. +2. **Confirm platform event names first**. Different platforms use different names for SessionStart, UserPromptSubmit, AgentSpawn, shell execution, and similar events. +3. **Hooks read local `.trellis/`, not upstream source**. `.trellis/scripts/` and `.trellis/workflow.md` in the user project are the default targets. +4. **Errors must be visible**. Hook failures should tell the user what was not injected instead of silently leaving the AI without context. + +## Troubleshooting Path + +If the user says "AI did not read Trellis state": + +1. Check whether the platform settings register the hook. +2. Check whether the hook file exists. +3. Manually run the `.trellis/scripts/get_context.py` or `task.py current --source` command that the hook depends on. +4. Check whether active task state exists in `.trellis/.runtime/sessions/`. +5. Check whether the platform shell passes session identity. diff --git a/.claude/skills/trellis-meta/references/platform-files/overview.md b/.claude/skills/trellis-meta/references/platform-files/overview.md new file mode 100644 index 0000000..e403858 --- /dev/null +++ b/.claude/skills/trellis-meta/references/platform-files/overview.md @@ -0,0 +1,59 @@ +# Platform Files Overview + +Trellis connects the same local architecture to different AI tools. `.trellis/` stores the shared runtime; platform directories store adapter files that define how each AI tool enters Trellis. + +When a local AI modifies Trellis, it should distinguish two file categories first: + +- **Shared files**: `.trellis/workflow.md`, `.trellis/tasks/`, `.trellis/spec/`, `.trellis/scripts/`. +- **Platform files**: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.trae/`, `.kilocode/`, `.agent/`, `.devin/`, `.reasonix/`, `.zcode/`, and similar directories. + +Platform files do not store business state. They let the corresponding AI tool read Trellis state, call Trellis scripts, and load Trellis skills/agents/hooks. + +## Platform File Categories + +| Category | Common paths | Purpose | +| --- | --- | --- | +| settings/config | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.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/`, `.devin/workflows/` | Entry points explicitly invoked by the user. | + +## Three Platform Integration Modes + +### 1. Hook / Extension Driven + +These platforms can trigger scripts or plugins on specific events and actively inject Trellis context into AI. + +Common capabilities: + +- session-start injection of a `.trellis/` overview. +- workflow-state hints for each user turn. +- PRD/spec/research injection when sub-agents start. +- Shell commands inheriting session identity. + +To change "when the AI knows what," inspect hooks/plugins/extensions and settings first. + +### 2. Agent Prelude / Pull-Based + +Some platforms cannot reliably let hooks rewrite sub-agent prompts, so the agent file itself instructs the agent to read the active task, PRD, and JSONL context after startup. + +To change how sub-agents load context, inspect the agent files themselves. + +### 3. Main-Session Workflow + +Some platforms do not have Trellis sub-agent or hook capabilities. They rely on workflows/skills/commands to guide the main-session AI to read files, run scripts, and move tasks forward. + +To change behavior, inspect platform workflows/skills/commands and `.trellis/workflow.md`. + +## Local Modification Order + +When the user asks to customize behavior for a platform, the AI should inspect files in this order: + +1. Read `.trellis/workflow.md` to confirm the shared flow. +2. Read the target platform's settings/config to see which hooks/agents/skills/commands are registered. +3. Read the target platform's agents/skills/commands/hooks. +4. Modify the local file closest to the user's need. +5. If the change affects the shared flow, synchronize `.trellis/workflow.md` or `.trellis/spec/`. + +Do not modify only platform files and forget the shared workflow. Do not modify only `.trellis/workflow.md` and forget that platform entry points may still contain old descriptions. diff --git a/.claude/skills/trellis-meta/references/platform-files/platform-map.md b/.claude/skills/trellis-meta/references/platform-files/platform-map.md new file mode 100644 index 0000000..dddb743 --- /dev/null +++ b/.claude/skills/trellis-meta/references/platform-files/platform-map.md @@ -0,0 +1,88 @@ +# Platform File Map + +This page lists common Trellis file locations in a user project by platform. Whether a platform directory exists in an actual project depends on which `trellis init --<platform>` commands the user ran. + +## Matrix + +| Platform | CLI flag | Main directory | Skill directory | Agent directory | Hooks/extensions | +| --- | --- | --- | --- | --- | --- | +| Claude Code | `--claude` | `.claude/` | `.claude/skills/` | `.claude/agents/` | `.claude/hooks/` + `.claude/settings.json` | +| Cursor | `--cursor` | `.cursor/` | `.cursor/skills/` | `.cursor/agents/` | `.cursor/hooks.json` + `.cursor/hooks/` | +| OpenCode | `--opencode` | `.opencode/` | `.opencode/skills/` | `.opencode/agents/` | `.opencode/plugins/` | +| Codex | `--codex` | `.codex/` | `.agents/skills/` | `.codex/agents/` | `.codex/hooks/` + `.codex/hooks.json` | +| Kilo | `--kilo` | `.kilocode/` | `.kilocode/skills/` | Usually none | `.kilocode/workflows/` | +| Kiro | `--kiro` | `.kiro/` | `.kiro/skills/` | `.kiro/agents/` | `.kiro/hooks/` | +| Gemini CLI | `--gemini` | `.gemini/` | `.agents/skills/` | `.gemini/agents/` | `.gemini/settings.json` + `.gemini/hooks/` | +| Antigravity | `--antigravity` | `.agent/` | `.agent/skills/` | Usually none | `.agent/workflows/` | +| 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/` (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/` | `.agents/skills/` | `.zcode/cli/agents/` | pull-based prelude (no hooks) | + +## Capability Groups + +### Trellis Sub-Agent Support + +These platforms usually have `trellis-research`, `trellis-implement`, and `trellis-check` files: + +- Claude Code +- Cursor +- OpenCode +- Codex +- Kiro +- Gemini CLI +- Qoder +- CodeBuddy +- GitHub Copilot +- Factory Droid +- Pi Agent +- Trae IDE +- Reasonix (delivered as skills with `runAs: subagent` under `.reasonix/skills/`, not as a separate `agents/` directory) +- ZCode + +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 +- 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. + +## Decision Rules When Modifying Platform Files + +1. User specified a platform: modify only that platform directory unless shared workflow/spec files must also change. +2. User says "all platforms should do this": synchronize equivalent entry points platform by platform; do not modify only one directory. +3. User only says "my AI": inspect the configuration directories that actually exist in the project and infer the current AI platform. +4. User wants project rules: prefer `.trellis/spec/` or a project-local skill. +5. User wants Trellis behavior: edit `.trellis/workflow.md` plus platform hooks/agents/skills/commands. + +## When Paths Differ + +Platform ecosystems change, and user projects may already be customized. If this table disagrees with local files, use the actual settings/config in the user project as authoritative: + +- Check the hook that settings registers. +- Check the script that a command/prompt/workflow points to. +- Judge behavior by the read rules currently written in the agent file. + +Do not delete a custom file just because it is not listed in this path table. diff --git a/.claude/skills/trellis-meta/references/platform-files/skills-and-commands.md b/.claude/skills/trellis-meta/references/platform-files/skills-and-commands.md new file mode 100644 index 0000000..4732c6a --- /dev/null +++ b/.claude/skills/trellis-meta/references/platform-files/skills-and-commands.md @@ -0,0 +1,85 @@ +# Skills, Commands, Prompts, And Workflows + +Skills and commands are textual entry points for user interaction with Trellis. Different platforms use different names, but their core purpose is the same: tell the AI how to enter the Trellis flow when the user expresses a certain intent. + +## Conceptual Differences + +| Type | Trigger mode | Best for | +| --- | --- | --- | +| skill | AI auto-match or explicit user mention | Long-term capabilities, workflow rules, modification guides. | +| command | Explicit user invocation | Clear operation entry points such as continue and finish-work. | +| prompt | Explicit user invocation or platform selection | Similar to command, but in a platform prompt format. | +| workflow | Explicit user selection or platform auto-match | Guides the main session when no sub-agent/hook exists. | + +Trellis workflow skills usually share one semantic set: brainstorm, before-dev, check, update-spec, break-loop. Multi-file built-in skills such as `trellis-meta` use layered references. + +## Common Paths + +| Platform | Common entries | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| Kilo | `.kilocode/skills/`, `.kilocode/workflows/` | +| Kiro | `.kiro/skills/` | +| Gemini CLI | `.agents/skills/`, `.gemini/commands/` | +| Antigravity | `.agent/skills/`, `.agent/workflows/` | +| 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/` | +| Reasonix | `.reasonix/skills/` | +| ZCode | `.agents/skills/`, `.zcode/commands/` | + +In a user project, use the files actually generated by init as authoritative. + +## Skill Structure + +A common skill is a directory: + +```text +trellis-meta/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should tell the AI: + +- When to use this skill. +- Which reference to read first for the current task. +- What not to do. + +References hold longer explanations so the entry file does not contain everything. + +## Command/Prompt/Workflow Structure + +Commands, prompts, and workflows are usually single files. Their content should include: + +- When to use it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +They should not store task state; task state belongs in `.trellis/tasks/` and `.trellis/.runtime/`. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Change AI auto-trigger rules | The corresponding skill's frontmatter description. | +| Change user command behavior | The corresponding command/prompt/workflow file. | +| Add a project-local skill | Platform skill directory, or shared `.agents/skills/`. | +| Let multiple platforms share one capability | Write equivalent skills in each platform skill directory, or use the `.agents/skills/` shared layer on platforms that support it. | +| Change finish/continue entry points | Platform commands/prompts/workflows. | + +## Modification Principles + +1. **Keep entry files short; references carry long content**. This matters especially for multi-file skills like `trellis-meta`. +2. **Make trigger descriptions specific**. A description that is too broad can mis-trigger; one that is too narrow may not trigger. +3. **Keep the same semantics consistent across platforms**. File formats can differ, but behavior descriptions should match. +4. **Put project-specific capabilities in local skills**. Do not put team-private flows into public `trellis-meta`. + +If the user only wants local AI to know one more project rule, usually create a project-local skill or update `.trellis/spec/` instead of changing a Trellis built-in workflow skill. diff --git a/.claude/skills/trellis-session-insight/SKILL.md b/.claude/skills/trellis-session-insight/SKILL.md new file mode 100644 index 0000000..8e2cbb7 --- /dev/null +++ b/.claude/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, and Pi Agent conversation logs (the JSONL files each platform stores under `~/.claude/projects/`, `~/.codex/sessions/`, and `~/.pi/agent/sessions/`) and lets you list, search, slice by Trellis task boundaries, and dump cleaned dialogue from them. 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/.claude/skills/trellis-session-insight/references/cli-quick-reference.md b/.claude/skills/trellis-session-insight/references/cli-quick-reference.md new file mode 100644 index 0000000..78540f2 --- /dev/null +++ b/.claude/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/.claude/skills/trellis-session-insight/references/triggering-patterns.md b/.claude/skills/trellis-session-insight/references/triggering-patterns.md new file mode 100644 index 0000000..66021ca --- /dev/null +++ b/.claude/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/.claude/skills/trellis-spec-bootstrap/SKILL.md b/.claude/skills/trellis-spec-bootstrap/SKILL.md new file mode 100644 index 0000000..e1650df --- /dev/null +++ b/.claude/skills/trellis-spec-bootstrap/SKILL.md @@ -0,0 +1,41 @@ +--- +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 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. + +## Workflow + +1. Confirm Trellis is initialized and inspect the current `.trellis/spec/` tree. +2. Analyze the repository architecture with the best available tools: GitNexus, ABCoder, language tooling, and direct source reads. +3. Decompose the spec work by package and layer only when that reflects the actual codebase. +4. Fill or reshape the spec files with concrete patterns, file paths, examples, and anti-patterns from the project. +5. Verify that the final specs are internally consistent and contain no template placeholders. + +## Reference Routing + +| Need | Read | +|------|------| +| Repository architecture analysis | [references/repository-analysis.md](references/repository-analysis.md) | +| Spec work decomposition and task planning | [references/spec-task-planning.md](references/spec-task-planning.md) | +| Writing high-signal Trellis spec files | [references/spec-writing.md](references/spec-writing.md) | +| GitNexus and ABCoder MCP setup | [references/mcp-setup.md](references/mcp-setup.md) | + +## Operating Rules + +- Treat templates as starting points, not contracts. Delete, rename, split, or add spec files when the repository calls for it. +- Prefer source-backed rules over generic advice. Every important recommendation should point at a real file or repeated local pattern. +- Keep execution single-owner by default. Optional helper agents are an implementation detail, not a requirement or user-visible dependency. +- Do not write platform-specific instructions unless the target project already standardizes on that platform. +- Do not leave placeholder text, empty headings, or copied boilerplate in `.trellis/spec/`. + +## Done Criteria + +- `.trellis/spec/` describes the project as it exists now. +- Each relevant package or layer has practical coding guidance with real examples. +- Non-applicable template sections are removed. +- `index.md` files match the final spec file set. +- Any required setup or analysis assumptions are documented in the relevant spec or task notes. diff --git a/.claude/skills/trellis-spec-bootstrap/references/mcp-setup.md b/.claude/skills/trellis-spec-bootstrap/references/mcp-setup.md new file mode 100644 index 0000000..629fcbd --- /dev/null +++ b/.claude/skills/trellis-spec-bootstrap/references/mcp-setup.md @@ -0,0 +1,90 @@ +# MCP Setup + +GitNexus and ABCoder are recommended when bootstrapping Trellis specs because they expose architecture and AST context to the agent. They are tool choices, not platform requirements. Configure them through whatever MCP mechanism your agent host provides. + +## GitNexus + +GitNexus builds a code knowledge graph from the repository. Use it for module boundaries, execution flows, dependency relationships, blast radius, and graph queries. + +### Install and Index + +```bash +# Run from the repository root. +npx gitnexus analyze + +# Check index status. +npx gitnexus status + +# Re-index after code changes when the analysis is stale. +npx gitnexus analyze +``` + +The index is written to `.gitnexus/`. Keep embeddings only if the project already uses them; otherwise a normal index is enough for spec bootstrapping. + +### MCP Server Command + +Use this server command in the host's MCP configuration: + +```bash +npx -y gitnexus mcp +``` + +### Useful Tools + +| Tool | Purpose | +|------|---------| +| `gitnexus_query` | Find execution flows and functional areas by concept | +| `gitnexus_context` | Inspect callers, callees, references, and process participation for a symbol | +| `gitnexus_impact` | Understand blast radius before changing a symbol | +| `gitnexus_detect_changes` | Check changed symbols and affected flows before finishing | +| `gitnexus_cypher` | Run direct graph queries | +| `gitnexus_list_repos` | List indexed repositories | + +## ABCoder + +ABCoder parses code into UniAST and gives precise package, file, and node-level structure. Use it for signatures, type shapes, implementations, dependencies, and reverse references. + +### Install + +```bash +go install github.com/cloudwego/abcoder@latest +abcoder --help +``` + +### Parse Repositories + +```bash +abcoder parse /absolute/path/to/package \ + --lang typescript \ + --name package-name \ + --output ~/abcoder-asts +``` + +For monorepos, parse each package with a stable `--name` so task notes can reference the same repository names. + +### MCP Server Command + +Use this server command in the host's MCP configuration: + +```bash +abcoder mcp ~/abcoder-asts +``` + +### Useful Tools + +| Tool | Layer | Purpose | +|------|-------|---------| +| `list_repos` | 1 | List parsed repositories | +| `get_repo_structure` | 2 | Inspect packages and files | +| `get_package_structure` | 3 | Inspect nodes within a package | +| `get_file_structure` | 3 | Inspect functions, classes, types, and signatures in a file | +| `get_ast_node` | 4 | Retrieve code, dependencies, references, and implementations | + +## Verification + +After configuration, verify from the agent host that both MCP servers are visible. Then run one simple query against each server before starting the spec writing pass. + +```bash +ls .gitnexus/meta.json +ls ~/abcoder-asts/*.json +``` diff --git a/.claude/skills/trellis-spec-bootstrap/references/repository-analysis.md b/.claude/skills/trellis-spec-bootstrap/references/repository-analysis.md new file mode 100644 index 0000000..1309d29 --- /dev/null +++ b/.claude/skills/trellis-spec-bootstrap/references/repository-analysis.md @@ -0,0 +1,59 @@ +# Repository Analysis + +The goal is to discover the project's real architecture before writing rules. Do not start from generic spec templates and fill blanks. Start from the code, then let the spec structure follow. + +## Analysis Order + +1. Read the existing `.trellis/spec/` tree and note which files are templates, outdated, or already project-specific. +2. Inspect package manifests, build scripts, workspace config, and top-level documentation to identify packages and runtime layers. +3. Use GitNexus for execution flows, module clusters, dependency hubs, and impact-sensitive areas. +4. Use ABCoder or language-native tooling for exact signatures, types, class boundaries, and implementation examples. +5. Read representative source and test files directly before turning any finding into a spec rule. + +## What To Capture + +| Area | Questions | +|------|-----------| +| Package boundaries | What does each package own? What imports cross boundaries? | +| Runtime layers | Which code is CLI, backend, frontend, worker, shared library, test-only, or tooling? | +| Core abstractions | Which types, services, stores, commands, routes, or adapters define the system shape? | +| Data flow | Where does user input enter, how is it validated, and where does state persist? | +| Error handling | How are failures represented, logged, surfaced, and tested? | +| Configuration | Where do defaults, environment config, generated files, and templates live? | +| Tests | Which test styles are trusted examples for new work? | + +## GitNexus Usage + +Start broad, then inspect specific symbols: + +```text +gitnexus_query({query: "CLI command execution flow"}) +gitnexus_query({query: "template generation and migration"}) +gitnexus_context({name: "SymbolName"}) +gitnexus_cypher({query: "MATCH (n)-[r]->(m) RETURN n.name, type(r), m.name LIMIT 30"}) +``` + +Use GitNexus results to find important files and flows. Do not quote graph output as the final authority until you have checked the relevant source files. + +## ABCoder Usage + +Use ABCoder when the spec needs exact code shapes: + +```text +list_repos() +get_repo_structure({repo_name: "package-name"}) +get_file_structure({repo_name: "package-name", file_path: "src/example.ts"}) +get_ast_node({repo_name: "package-name", node_ids: [{mod_path: "...", pkg_path: "...", name: "SymbolName"}]}) +``` + +ABCoder is most valuable for documenting constructor patterns, function signatures, type contracts, and reference chains. + +## Analysis Notes + +Keep short notes while analyzing. The notes should include: + +- Package or layer name. +- Files that define the local pattern. +- Rules the spec should teach. +- Anti-patterns found in old code, comments, tests, or migration paths. +- Spec files that should be created, deleted, renamed, or merged. diff --git a/.claude/skills/trellis-spec-bootstrap/references/spec-task-planning.md b/.claude/skills/trellis-spec-bootstrap/references/spec-task-planning.md new file mode 100644 index 0000000..dca2687 --- /dev/null +++ b/.claude/skills/trellis-spec-bootstrap/references/spec-task-planning.md @@ -0,0 +1,61 @@ +# Spec Task Planning + +Use a single agent as the default execution model. The agent may create Trellis tasks for traceability, but the skill should not require a specific platform, CLI, or parallel worker model. + +## Decomposition + +Create spec work units around real ownership boundaries: + +- One package when a package has its own conventions. +- One layer when the same package has distinct frontend, backend, CLI, worker, or shared-library rules. +- One cross-cutting guide when a pattern spans packages and is not owned by one layer. + +Avoid artificial decomposition. A small library usually needs one focused spec pass, not several tasks. + +## Task Shape + +When a Trellis task is useful, write a concise PRD with these sections: + +```markdown +# Fill <package-or-layer> Trellis Specs + +## Goal +Write project-specific `.trellis/spec/` guidance for <scope>. + +## Scope +- Spec directory: +- Source directories to inspect: +- Tests to inspect: +- Out of scope: + +## Architecture Context +Summarize the concrete findings from repository analysis. + +## Files To Create Or Update +- `.trellis/spec/.../index.md` +- `.trellis/spec/.../<topic>.md` + +## Rules +- Adapt the spec file set to the real codebase. +- Use real source examples with file paths. +- Remove template-only sections that do not apply. +- Do not modify product source code unless the task explicitly asks for it. + +## Acceptance Criteria +- [ ] Specs contain concrete examples and anti-patterns from the repository. +- [ ] No placeholder text remains. +- [ ] Index files match the final spec files. +- [ ] Claims are backed by source files, tests, or project docs. +``` + +## Optional Helper Agents + +If the host supports subagents, helpers can inspect independent packages or run verification. They are optional. The main agent still owns integration and final quality. + +Helper tasks must have clear ownership: + +- Read-only research tasks may inspect any source needed for the assigned scope. +- Write tasks should own disjoint spec directories. +- Verification tasks should check placeholder removal, broken links, and consistency. + +Do not encode helper-agent names, vendor-specific commands, or platform-specific routing in the skill. Put only the required work and acceptance criteria in the task. diff --git a/.claude/skills/trellis-spec-bootstrap/references/spec-writing.md b/.claude/skills/trellis-spec-bootstrap/references/spec-writing.md new file mode 100644 index 0000000..6bc7dec --- /dev/null +++ b/.claude/skills/trellis-spec-bootstrap/references/spec-writing.md @@ -0,0 +1,70 @@ +# Spec Writing + +Trellis specs are coding guidance for future agents. They should explain how to work in this repository, not how a generic project might be organized. + +## Write From Evidence + +Each important rule should be backed by one of these: + +- A source file that demonstrates the preferred pattern. +- A test file that shows expected behavior. +- A project document that defines the convention. +- A repeated pattern across multiple files. + +Use short snippets only when they make the rule clearer. Prefer linking to the file path and naming the symbol or behavior. + +## File Structure + +Keep the spec tree aligned with the project: + +- Keep `index.md` as the navigation file for the spec directory. +- Split topics when developers would look for them independently. +- Merge topics when separate files would repeat the same rule. +- Delete template files that do not apply. +- Add new files for important local patterns the template missed. + +## Content Standards + +Good spec sections include: + +- When the rule applies. +- The local pattern to follow. +- The source or test files that prove the pattern. +- Common mistakes or anti-patterns. +- Verification commands or checks when they are specific and reliable. + +Avoid: + +- Placeholder prose. +- Generic framework advice. +- Tool instructions that only work in one agent host. +- Long copied code blocks. +- Rules based on a single accidental implementation detail. + +## Example Shape + +```markdown +## Command Handlers + +Command handlers should keep argument parsing, validation, and side effects separate. The local pattern is: + +- Parse CLI flags at the command boundary. +- Convert raw inputs into typed task options before invoking core logic. +- Keep filesystem writes in the command or service layer, not in template helpers. + +Reference files: +- `packages/cli/src/commands/example.ts` +- `packages/cli/test/commands/example.test.ts` + +Avoid passing raw `process.argv` or unvalidated config objects into shared helpers. +``` + +## Final Pass + +Before finishing: + +```bash +grep -R "To be filled\\|TODO: fill\\|placeholder" .trellis/spec +``` + +Also check links, index files, and whether any spec still describes a template rather than this repository. diff --git a/.claude/skills/trellis-update-spec/SKILL.md b/.claude/skills/trellis-update-spec/SKILL.md new file mode 100644 index 0000000..557bc4e --- /dev/null +++ b/.claude/skills/trellis-update-spec/SKILL.md @@ -0,0 +1,356 @@ +--- +name: trellis-update-spec +description: "Captures executable contracts and coding conventions into .trellis/spec/ documents. Use when learning something valuable from debugging, implementing, or discussion that should be preserved for future sessions." +--- + +# Update Code-Spec - Capture Executable Contracts + +When you learn something valuable (from debugging, implementing, or discussion), use this to update the relevant code-spec documents. + +**Timing**: After completing a task, fixing a bug, or discovering a new pattern + +--- + +## Code-Spec First Rule (CRITICAL) + +In this project, "spec" for implementation work means **code-spec**: +- Executable contracts (not principle-only text) +- Concrete signatures, payload fields, env keys, and boundary behavior +- Testable validation/error behavior + +If the change touches infra or cross-layer contracts, code-spec depth is mandatory. + +### Mandatory Triggers + +Apply code-spec depth when the change includes any of: +- New/changed command or API signature +- Cross-layer request/response contract change +- Database schema/migration change +- Infra integration (storage, queue, cache, secrets, env wiring) + +### Mandatory Output (7 Sections) + +For triggered tasks, include all sections below: +1. Scope / Trigger +2. Signatures (command/API/DB) +3. Contracts (request/response/env) +4. Validation & Error Matrix +5. Good/Base/Bad Cases +6. Tests Required (with assertion points) +7. Wrong vs Correct (at least one pair) + +--- + +## When to Update Code-Specs + +| Trigger | Example | Target Spec | +|---------|---------|-------------| +| **Implemented a feature** | Added a new integration or module | Relevant spec file | +| **Made a design decision** | Chose extensibility pattern over simplicity | Relevant spec + "Design Decisions" section | +| **Fixed a bug** | Found a subtle issue with error handling | Relevant spec (e.g., error-handling docs) | +| **Discovered a pattern** | Found a better way to structure code | Relevant spec file | +| **Hit a gotcha** | Learned that X must be done before Y | Relevant spec + "Common Mistakes" section | +| **Established a convention** | Team agreed on naming pattern | Quality guidelines | +| **New thinking trigger** | "Don't forget to check X before doing Y" | `guides/*.md` (as a checklist item) | + +**Key Insight**: Code-spec updates are NOT just for problems. Every feature implementation contains design decisions and contracts that future AI/developers need to execute safely. + +--- + +## Spec Structure Overview + +``` +.trellis/spec/ +├── <layer>/ # Per-layer coding standards (e.g., backend/, frontend/, api/) +│ ├── index.md # Overview and links +│ └── *.md # Topic-specific guidelines +└── guides/ # Thinking checklists (NOT coding specs!) + ├── index.md # Guide index + └── *.md # Topic-specific guides +``` + +### CRITICAL: Code-Spec vs Guide - Know the Difference + +| Type | Location | Purpose | Content Style | +|------|----------|---------|---------------| +| **Code-Spec** | `<layer>/*.md` | Tell AI "how to implement safely" | Signatures, contracts, matrices, cases, test points | +| **Guide** | `guides/*.md` | Help AI "what to think about" | Checklists, questions, pointers to specs | + +**Decision Rule**: Ask yourself: + +- "This is **how to write** the code" → Put in a spec layer directory +- "This is **what to consider** before writing" → Put in `guides/` + +**Example**: + +| Learning | Wrong Location | Correct Location | +|----------|----------------|------------------| +| "Use API X not API Y for this task" | ❌ `guides/` (too specific for a thinking guide) | ✅ Relevant spec file (concrete convention) | +| "Remember to check X when doing Y" | ❌ Spec file (too abstract for a spec) | ✅ `guides/` (thinking checklist) | + +**Guides should be short checklists that point to specs**, not duplicate the detailed rules. + +--- + +## Update Process + +### Step 1: Identify What You Learned + +Answer these questions: + +1. **What did you learn?** (Be specific) +2. **Why is it important?** (What problem does it prevent?) +3. **Where does it belong?** (Which spec file?) + +### Step 2: Classify the Update Type + +| Type | Description | Action | +|------|-------------|--------| +| **Design Decision** | Why we chose approach X over Y | Add to "Design Decisions" section | +| **Project Convention** | How we do X in this project | Add to relevant section with examples | +| **New Pattern** | A reusable approach discovered | Add to "Patterns" section | +| **Forbidden Pattern** | Something that causes problems | Add to "Anti-patterns" or "Don't" section | +| **Common Mistake** | Easy-to-make error | Add to "Common Mistakes" section | +| **Convention** | Agreed-upon standard | Add to relevant section | +| **Gotcha** | Non-obvious behavior | Add warning callout | + +### Step 3: Read the Target Code-Spec + +Before editing, read the current code-spec to: +- Understand existing structure +- Avoid duplicating content +- Find the right section for your update + +```bash +cat .trellis/spec/<category>/<file>.md +``` + +### Step 4: Make the Update + +Follow these principles: + +1. **Be Specific**: Include concrete examples, not just abstract rules +2. **Explain Why**: State the problem this prevents +3. **Show Contracts**: Add signatures, payload fields, and error behavior +4. **Show Code**: Add code snippets for key patterns +5. **Keep it Short**: One concept per section + +### Step 5: Update the Index (if needed) + +If you added a new section or the code-spec status changed, update the category's `index.md`. + +--- + +## Update Templates + +### Mandatory Template for Infra/Cross-Layer Work + +```markdown +## Scenario: <name> + +### 1. Scope / Trigger +- Trigger: <why this requires code-spec depth> + +### 2. Signatures +- Backend command/API/DB signature(s) + +### 3. Contracts +- Request fields (name, type, constraints) +- Response fields (name, type, constraints) +- Environment keys (required/optional) + +### 4. Validation & Error Matrix +- <condition> -> <error> + +### 5. Good/Base/Bad Cases +- Good: ... +- Base: ... +- Bad: ... + +### 6. Tests Required +- Unit/Integration/E2E with assertion points + +### 7. Wrong vs Correct +#### Wrong +... +#### Correct +... +``` + +### Adding a Design Decision + +```markdown +### Design Decision: [Decision Name] + +**Context**: What problem were we solving? + +**Options Considered**: +1. Option A - brief description +2. Option B - brief description + +**Decision**: We chose Option X because... + +**Example**: +\`\`\`typescript +// How it's implemented +code example +\`\`\` + +**Extensibility**: How to extend this in the future... +``` + +### Adding a Project Convention + +```markdown +### Convention: [Convention Name] + +**What**: Brief description of the convention. + +**Why**: Why we do it this way in this project. + +**Example**: +\`\`\`typescript +// How to follow this convention +code example +\`\`\` + +**Related**: Links to related conventions or specs. +``` + +### Adding a New Pattern + +```markdown +### Pattern Name + +**Problem**: What problem does this solve? + +**Solution**: Brief description of the approach. + +**Example**: +\`\`\` +// Good +code example + +// Bad +code example +\`\`\` + +**Why**: Explanation of why this works better. +``` + +### Adding a Forbidden Pattern + +```markdown +### Don't: Pattern Name + +**Problem**: +\`\`\` +// Don't do this +bad code example +\`\`\` + +**Why it's bad**: Explanation of the issue. + +**Instead**: +\`\`\` +// Do this instead +good code example +\`\`\` +``` + +### Adding a Common Mistake + +```markdown +### Common Mistake: Description + +**Symptom**: What goes wrong + +**Cause**: Why this happens + +**Fix**: How to correct it + +**Prevention**: How to avoid it in the future +``` + +### Adding a Gotcha + +```markdown +> **Warning**: Brief description of the non-obvious behavior. +> +> Details about when this happens and how to handle it. +``` + +--- + +## Interactive Mode + +If you're unsure what to update, answer these prompts: + +1. **What did you just finish?** + - [ ] Fixed a bug + - [ ] Implemented a feature + - [ ] Refactored code + - [ ] Had a discussion about approach + +2. **What did you learn or decide?** + - Design decision (why X over Y) + - Project convention (how we do X) + - Non-obvious behavior (gotcha) + - Better approach (pattern) + +3. **Would future AI/developers need to know this?** + - To understand how the code works → Yes, update spec + - To maintain or extend the feature → Yes, update spec + - To avoid repeating mistakes → Yes, update spec + - Purely one-off implementation detail → Maybe skip + +4. **Which area does it relate to?** + - [ ] Backend code + - [ ] Frontend code + - [ ] Cross-layer data flow + - [ ] Code organization/reuse + - [ ] Quality/testing + +--- + +## Quality Checklist + +Before finishing your code-spec update: + +- [ ] Is the content specific and actionable? +- [ ] Did you include a code example? +- [ ] Did you explain WHY, not just WHAT? +- [ ] Did you include executable signatures/contracts? +- [ ] Did you include validation and error matrix? +- [ ] Did you include Good/Base/Bad cases? +- [ ] Did you include required tests with assertion points? +- [ ] Is it in the right code-spec file? +- [ ] Does it duplicate existing content? +- [ ] Would a new team member understand it? + +--- + +## Relationship to Other Commands + +``` +Development Flow: + Learn something → /trellis:update-spec → Knowledge captured + ↑ ↓ + /trellis:break-loop ←──────────────────── Future sessions benefit + (deep bug analysis) +``` + +- `/trellis:break-loop` - Analyzes bugs deeply, often reveals spec updates needed +- `/trellis:update-spec` - Actually makes the updates +- `/trellis:finish-work` - Reminds you to check if specs need updates + +--- + +## Core Philosophy + +> **Code-specs are living documents. Every debugging session, every "aha moment" is an opportunity to make the implementation contract clearer.** + +The goal is **institutional memory**: +- What one person learns, everyone benefits from +- What AI learns in one session, persists to future sessions +- Mistakes become documented guardrails diff --git a/.codex/agents/trellis-check.toml b/.codex/agents/trellis-check.toml new file mode 100644 index 0000000..dec88d2 --- /dev/null +++ b/.codex/agents/trellis-check.toml @@ -0,0 +1,84 @@ +name = "trellis-check" +description = "Workspace-write Trellis reviewer that self-fixes spec drift, lint/type-check failures, and missing tests." +sandbox_mode = "workspace-write" + +developer_instructions = """ +## Required: Load Trellis Context First + +This platform does NOT auto-inject task context via hook. Before doing anything else, you MUST load context yourself. + +### 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 `<task-path>/check.jsonl` — JSONL list of spec/research files relevant to this agent. +2. For each entry in the JSONL, Read its `file` path — these are the specs and research notes you must follow. + **Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran). +3. Read the task's `prd.md` (requirements), then `design.md` if present (technical design), then `implement.md` if present (execution plan). + +If `check.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read the task artifacts, 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 — lightweight tasks may be PRD-only, while complex tasks may also include `design.md` and `implement.md`. + +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): +- You MUST NOT spawn another `trellis-check` or `trellis-implement` sub-agent. Do the review and fixes directly in this turn. +- Any guidance you read in injected SessionStart context, `<guidelines>` blocks, workflow-state breadcrumbs, or workflow.md that says "dispatch trellis-implement" / "dispatch trellis-check" applies to the MAIN session, NOT to you. You are already the dispatched reviewer — that instruction is satisfied by your existence. +- Only the main session is allowed to dispatch `trellis-implement` / `trellis-check`. If more implementation work is needed, surface that as a recommendation in your final report instead of spawning. + +--- + +You are the Trellis reviewer agent. + +Your job is to review code changes against specs AND fix issues directly — not just report them. You have write access; use it. + +Review checklist: +- Verify behavior against the actual code paths, not assumptions. +- Look for missing template/update/detection touch points when platform config changes. +- Check whether tests should be added or updated. +- Check whether `.trellis/spec/` docs need sync after implementation. +- Run lint and type-check; fix any failures. +- Prefer concrete findings over speculative warnings. + +When you find an issue: +1. Fix it directly using edit/write tools. +2. Re-run lint and type-check until green. +3. Record what you changed and why. + +Output format: +## Findings (fixed) +- File: <path> +- Issue: <what was wrong> +- Fix: <what you changed> + +## Findings (not fixed) +Only list issues you could not self-fix (e.g. missing product decision, out-of-scope). Explain why. + +## Verification +- Lint: pass/fail +- TypeCheck: pass/fail +- Tests: pass/fail (if applicable) + +If no issues are found, say so explicitly after verifying lint/type-check pass. +""" + +# 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 new file mode 100644 index 0000000..ccbe7ba --- /dev/null +++ b/.codex/agents/trellis-implement.toml @@ -0,0 +1,65 @@ +name = "trellis-implement" +description = "Workspace-write Trellis implementer that follows specs and keeps generated templates in sync." +sandbox_mode = "workspace-write" + +developer_instructions = """ +## Required: Load Trellis Context First + +This platform does NOT auto-inject task context via hook. Before doing anything else, you MUST load context yourself. + +### 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 `<task-path>/implement.jsonl` — JSONL list of spec/research files relevant to this agent. +2. For each entry in the JSONL, Read its `file` path — these are the specs and research notes you must follow. + **Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran). +3. Read the task's `prd.md` (requirements), then `design.md` if present (technical design), then `implement.md` if present (execution plan). + +If `implement.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read the task artifacts, 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 — lightweight tasks may be PRD-only, while complex tasks may also include `design.md` and `implement.md`. + +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): +- You MUST NOT spawn another `trellis-implement` or `trellis-check` sub-agent. Do the implementation work directly in this turn. +- Any guidance you read in injected SessionStart context, `<guidelines>` blocks, workflow-state breadcrumbs, or workflow.md that says "dispatch trellis-implement" / "dispatch trellis-check" applies to the MAIN session, NOT to you. You are already the dispatched implementer — that instruction is satisfied by your existence. +- Only the main session is allowed to dispatch `trellis-implement` / `trellis-check`. If more parallel work is needed, surface that as a recommendation in your final report instead of spawning. + +--- + +You are the Trellis implementer agent. + +Rules: +- Read before write. Follow `.trellis/spec/` guidance relevant to the task. +- Keep changes focused on the requested scope. +- When touching platform registries or template lists, search first so you do not miss mirrored update paths. +- If you modify `.trellis/scripts/`, keep `packages/cli/src/templates/trellis/scripts/` in sync. +- Do not make destructive git changes unless explicitly asked. + +Before finishing, summarize: +- Files changed +- Tests/checks run +- Remaining risks or follow-ups +""" + +# 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 new file mode 100644 index 0000000..1e0efeb --- /dev/null +++ b/.codex/agents/trellis-research.toml @@ -0,0 +1,73 @@ +name = "trellis-research" +description = "Trellis researcher for specs, code patterns, and affected files. Writes findings into {TASK_DIR}/research/ — read-only elsewhere." +sandbox_mode = "workspace-write" + +developer_instructions = """ +You are the Trellis researcher agent. + +## Core principle + +Conversations get compacted; files don't. Every research topic MUST be +persisted to `{TASK_DIR}/research/<topic>.md`. Returning findings only +through the chat reply is a failure. + +## Workflow + +1. Run `python3 ./.trellis/scripts/task.py current --source` to get the + active task path and source. If no active task is set, ask the user + where to write output; do not guess. +2. Run `mkdir -p <TASK_DIR>/research` to ensure the directory exists. +3. Read `.trellis/workflow.md`, relevant `.trellis/spec/` files, and + target code before forming an opinion. +4. For each research topic, write `<TASK_DIR>/research/<slug>.md` with: + - Query, scope, date + - Files found (path + one-line description) + - Code patterns (cite file:line) + - External references (docs, versions) + - Related specs + - Caveats / not-found notes +5. Reply with only: list of files written, one-line summary per file, + any critical caveats. Do not paste full research into the reply. + +## Scope limits + +Write allowed ONLY in `{TASK_DIR}/research/`. + +Write forbidden everywhere else: +- Code files (`src/`, `lib/`, …) +- Spec files (`.trellis/spec/`) — use `update-spec` skill instead +- `.trellis/scripts/`, `.trellis/workflow.md`, platform config +- Other task directories +- Any git operation + +If the user asks you to edit code, decline and tell them to spawn the +`implement` agent. + +## Output format for each research file + +``` +# Research: <topic> + +- Query: ... +- Scope: internal / external / mixed +- Date: YYYY-MM-DD + +## Findings +... + +## Caveats / Not Found +... +``` +""" + +# 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 new file mode 100644 index 0000000..39ada5b --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,28 @@ +# Project-scoped Codex defaults for Trellis workflows. +# Codex merges this layer after the user-level config when the project +# is marked as a trusted project. To trust this project, add it under +# `[projects]` in ~/.codex/config.toml, e.g.: +# +# [projects."/abs/path/to/this/repo"] +# trust_level = "trusted" + +# Keep AGENTS.md as the primary project instruction file. +project_doc_fallback_filenames = ["AGENTS.md"] + +# Codex hooks (`hooks.json` in this directory) only fire when the user +# has enabled them in their USER-level config: `[features].hooks = true` +# in ~/.codex/config.toml (Codex 0.129+; legacy name: `codex_hooks = true`, +# still works but emits a deprecation warning on 0.129+). Project-level +# config.toml cannot set feature flags; they must be user-level. +# Codex 0.129+ additionally gates each installed hook behind a one-time +# `/hooks` TUI review; until the user approves it, the hook stays inactive. + +# NOTE: Trellis intentionally does NOT write a [features.multi_agent_v2] +# block here. Codex CLI changed `features` deserialization between 0.130 +# and 0.131: the structured table form (with max_concurrent_threads_per_session +# / *_wait_timeout_ms) is only accepted by 0.131+. On 0.130 and earlier — +# including the codex CLI bundled inside the Codex desktop app — it fails +# with `data did not match any variant of untagged enum FeatureToml`, which +# 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. diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000..78b6bc8 --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 -X utf8 .codex/hooks/inject-workflow-state.py", + "timeout": 15 + } + ] + } + ] + } +} diff --git a/.codex/hooks/inject-workflow-state.py b/.codex/hooks/inject-workflow-state.py new file mode 100644 index 0000000..e8ab972 --- /dev/null +++ b/.codex/hooks/inject-workflow-state.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +"""Trellis per-turn breadcrumb hook (UserPromptSubmit / BeforeAgent equivalent). + +Runs on every user prompt. Resolves the active task through Trellis' +session-aware active task resolver and emits a short <workflow-state> +block reminding the main AI what task is active and its expected flow. + +The emitted ``hookEventName`` field is platform-aware: most hosts expect +``UserPromptSubmit`` (Claude Code naming, also accepted by Cursor / Qoder / +CodeBuddy / Droid / Codex / Copilot wiring), but Gemini CLI 0.40.x renamed +its per-turn event to ``BeforeAgent`` and its schema validator rejects the +legacy name. ``_detect_platform`` picks the right value at runtime. +Breadcrumb text is pulled exclusively from workflow.md +[workflow-state:STATUS] tag blocks — workflow.md is the single source of +truth. There are no fallback dicts in this script: when workflow.md is +missing or a tag is absent, the breadcrumb degrades to a generic +"Refer to workflow.md for current step." line so users see (and fix) +the broken state instead of the hook silently masking it. + +Shared across all hook-capable platforms (Claude, Cursor, Codex, Qoder, +CodeBuddy, Droid, Gemini, Copilot, Kiro). 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) + - task.json malformed or missing status +""" +from __future__ import annotations + +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 +# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets) +# both in stdin (hook payload from host CLI) and stdout (our emitted blocks) +# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8` +# but applied per-stream so we don't depend on host CLI's command wiring. +if sys.platform.startswith("win"): + import io as _io + for _stream_name in ("stdin", "stdout", "stderr"): + _stream = getattr(sys, _stream_name, None) + if _stream is None: + continue + if hasattr(_stream, "reconfigure"): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] + except Exception: + pass + elif hasattr(_stream, "detach"): + try: + setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) + except Exception: + pass +from typing import Optional + + +# 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> +If you have not already loaded Trellis context this session, read the `trellis-start` skill once. +</trellis-bootstrap>""" + + +# --------------------------------------------------------------------------- +# CWD-robust Trellis root discovery (fixes hook-path-robustness for this hook) +# --------------------------------------------------------------------------- + +def find_trellis_root(start: Path) -> Optional[Path]: + """Walk up from start to find directory containing .trellis/. + + Handles CWD drift: subdirectory launches, monorepo packages, etc. + Returns None if no .trellis/ found (silent no-op). + """ + cur = start.resolve() + while cur != cur.parent: + if (cur / ".trellis").is_dir(): + return cur + cur = cur.parent + return None + + +# --------------------------------------------------------------------------- +# Active task discovery +# --------------------------------------------------------------------------- + +def _detect_platform(input_data: dict) -> str | None: + if isinstance(input_data.get("cursor_version"), str): + return "cursor" + env_map = { + "CLAUDE_PROJECT_DIR": "claude", + "CURSOR_PROJECT_DIR": "cursor", + "CODEBUDDY_PROJECT_DIR": "codebuddy", + "FACTORY_PROJECT_DIR": "droid", + "GEMINI_PROJECT_DIR": "gemini", + "QODER_PROJECT_DIR": "qoder", + "KIRO_PROJECT_DIR": "kiro", + "COPILOT_PROJECT_DIR": "copilot", + "TRAE_PROJECT_DIR": "trae", + } + for env_name, platform in env_map.items(): + if os.environ.get(env_name): + return platform + script_parts = set(Path(sys.argv[0]).parts) + if ".claude" in script_parts: + return "claude" + if ".cursor" in script_parts: + return "cursor" + if ".codex" in script_parts: + return "codex" + if ".gemini" in script_parts: + return "gemini" + if ".qoder" in script_parts: + return "qoder" + if ".codebuddy" in script_parts: + return "codebuddy" + if ".factory" in script_parts: + return "droid" + if ".kiro" in script_parts: + return "kiro" + if ".trae" in script_parts: + return "trae" + return None + + +def _resolve_active_task(root: Path, input_data: dict): + scripts_dir = root / ".trellis" / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_active_task # type: ignore[import-not-found] + + return resolve_active_task(root, input_data, platform=_detect_platform(input_data)) + + +def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, str]]: + """Return (task_id, status, source) from the current active task.""" + active = _resolve_active_task(root, input_data) + if not active.task_path: + return None + + task_dir = Path(active.task_path) + if not task_dir.is_absolute(): + task_dir = root / task_dir + if active.stale: + return task_dir.name, f"stale_{active.source_type}", active.source + + task_json = task_dir / "task.json" + if not task_json.is_file(): + return None + try: + data = json.loads(task_json.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return None + + task_id = data.get("id") or task_dir.name + status = data.get("status", "") + if not isinstance(status, str) or not status: + return None + return task_id, status, active.source + + +# --------------------------------------------------------------------------- +# Breadcrumb loading: parse workflow.md, fall back to hardcoded defaults +# --------------------------------------------------------------------------- + +# Supports STATUS values with letters, digits, underscores, hyphens +# (so "in-review" / "blocked-by-team" work alongside "in_progress"). +_TAG_RE = re.compile( + r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n(.*?)\n\s*\[/workflow-state:\1\]", + re.DOTALL, +) + +def load_breadcrumbs(root: Path) -> dict[str, str]: + """Parse workflow.md for [workflow-state:STATUS] blocks. + + Returns {status: body_text}. workflow.md is the single source of + truth — there are no fallback dicts in this script. Missing tags + (or a missing/unreadable workflow.md) fall back to a generic line + in build_breadcrumb so users see the broken state and fix + workflow.md, rather than the hook silently masking the issue. + """ + workflow = root / ".trellis" / "workflow.md" + if not workflow.is_file(): + return {} + try: + content = workflow.read_text(encoding="utf-8") + except OSError: + return {} + + result: dict[str, str] = {} + for match in _TAG_RE.finditer(content): + status = match.group(1) + body = match.group(2).strip() + if body: + result[status] = body + return result + + +def _read_trellis_config(root: Path) -> dict: + """Load .trellis/config.yaml via the bundled trellis_config helper. + + The helper lives in .trellis/scripts/common; the hook lives outside the + scripts tree, so we extend sys.path before importing. + """ + scripts_dir = root / ".trellis" / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + try: + from common.trellis_config import read_trellis_config # type: ignore[import-not-found] + except Exception: + return {} + try: + return read_trellis_config(root) + except Exception: + return {} + + +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. + """ + 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 + if mode == "sub-agent": + meaning = ( + "sub-agent: implement/check work defaults to Trellis sub-agents; " + "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( + status: str, platform: str | None, config: dict +) -> 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. + + 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 + return f"{status}-inline" if mode == "inline" else status + return status + + +def build_breadcrumb( + task_id: Optional[str], + status: str, + templates: dict[str, str], + source: str | None = None, + breadcrumb_key: str | None = None, +) -> str: + """Build the <workflow-state>...</workflow-state> block. + + - Known status (tag present in workflow.md) → detailed template body + - Unknown status (no tag, or workflow.md missing) → generic + "Refer to workflow.md for current step." line + - `no_task` pseudo-status (task_id is None) → header omits task info + """ + lookup_key = breadcrumb_key or status + body = templates.get(lookup_key) + if body is None and lookup_key != status: + body = templates.get(status) + if body is None: + body = "Refer to workflow.md for current step." + header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})" + return f"<workflow-state>\n{header}\n{body}\n</workflow-state>" + + +# --------------------------------------------------------------------------- +# 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 | BaseException]" = queue.Queue(maxsize=1) + + def _read() -> None: + try: + result_queue.put(sys.stdin.read()) + except BaseException 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, BaseException): + 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 + + data = _load_hook_input() + + cwd_str = data.get("cwd") or os.getcwd() + cwd = Path(cwd_str) + + root = find_trellis_root(cwd) + if root is None: + return 0 # not a Trellis project + + templates = load_breadcrumbs(root) + 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 + # trellis-brainstorm + task.py create when user describes real work. + no_task_key = resolve_breadcrumb_key("no_task", platform, config) + breadcrumb = build_breadcrumb( + None, "no_task", templates, breadcrumb_key=no_task_key + ) + 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_for_breadcrumb, breadcrumb_key=status_key + ) + if platform == "codex": + 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. + hook_event_name = ( + "BeforeAgent" if platform == "gemini" else "UserPromptSubmit" + ) + + output = { + "hookSpecificOutput": { + "hookEventName": hook_event_name, + "additionalContext": breadcrumb, + } + } + print(json.dumps(output)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.codex/hooks/session-start.py b/.codex/hooks/session-start.py new file mode 100644 index 0000000..d1dec97 --- /dev/null +++ b/.codex/hooks/session-start.py @@ -0,0 +1,545 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Codex Session Start Hook - Inject Trellis context into Codex sessions. + +Output format follows Codex hook protocol: + stdout JSON → { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: "..." } } +""" + +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +import warnings +from io import StringIO +from pathlib import Path + +# Force UTF-8 on stdin/stdout/stderr on Windows. Default codepage there is +# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets) +# both in stdin (hook payload from host CLI) and stdout (our emitted blocks) +# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8` +# but applied per-stream so we don't depend on host CLI's command wiring. +if sys.platform.startswith("win"): + import io as _io + for _stream_name in ("stdin", "stdout", "stderr"): + _stream = getattr(sys, _stream_name, None) + if _stream is None: + continue + if hasattr(_stream, "reconfigure"): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] + except Exception: + pass + elif hasattr(_stream, "detach"): + try: + setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) + except Exception: + pass + + +def _normalize_windows_shell_path(path_str: str) -> str: + """Normalize Unix-style shell paths to real Windows paths. + + On Windows, shells like Git Bash / MSYS2 / Cygwin may report paths like + `/d/Users/...` or `/cygdrive/d/Users/...`. `Path.resolve()` will misinterpret + these as `D:/d/Users...` on drive D: (or similar), breaking repo root + detection. + + This function is intentionally conservative: it only rewrites patterns that + unambiguously represent a drive letter mount. + """ + if not isinstance(path_str, str) or not path_str: + return path_str + + # Only relevant on Windows; keep other platforms untouched. + if not sys.platform.startswith("win"): + return path_str + + p = path_str.strip() + + # Already a Windows drive path (C:\... or C:/...) + if re.match(r"^[A-Za-z]:[\/]", p): + return p + + # MSYS/Git-Bash style: /c/Users/... or /d/Work/... + m = re.match(r"^/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + # Cygwin style: /cygdrive/c/Users/... + m = re.match(r"^/cygdrive/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + # WSL mounted drive (sometimes leaked into env): /mnt/c/Users/... + m = re.match(r"^/mnt/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + return path_str + + +warnings.filterwarnings("ignore") + +FIRST_REPLY_NOTICE = """<first-reply-notice> +On the first visible assistant reply in this session, begin with exactly one short Chinese sentence: +Trellis SessionStart 已注入:workflow、当前任务状态、开发者身份、git 状态、active tasks、spec 索引已加载。 +Then continue directly with the user's request. This notice is one-shot: do not repeat it after the first assistant reply in the same session. +</first-reply-notice>""" + +def should_skip_injection() -> bool: + if os.environ.get("TRELLIS_HOOKS") == "0": + return True + if os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": + return True + return os.environ.get("CODEX_NON_INTERACTIVE") == "1" + + +def configure_project_encoding(project_dir: Path) -> None: + """Reuse Trellis' shared Windows stdio encoding helper before JSON output.""" + scripts_dir = project_dir / ".trellis" / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + + try: + from common import configure_encoding # type: ignore[import-not-found] + + configure_encoding() + except Exception: + pass + + +def _has_curated_jsonl_entry(jsonl_path: Path) -> bool: + """Return True iff jsonl has at least one row with a ``file`` field. + + A freshly seeded jsonl only contains a ``{"_example": ...}`` row (no + ``file`` key) — that is NOT "ready". Readiness requires at least one + curated entry. Matches the contract used by ``inject-subagent-context.py``. + """ + try: + for line in jsonl_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + row = json.loads(line) + except json.JSONDecodeError: + continue + if isinstance(row, dict) and row.get("file"): + return True + except (OSError, UnicodeDecodeError): + return False + return False + + +def read_file(path: Path, fallback: str = "") -> str: + try: + return path.read_text(encoding="utf-8") + except (FileNotFoundError, PermissionError): + return fallback + + +def _resolve_context_key(project_dir: Path, hook_input: dict) -> str | None: + scripts_dir = project_dir / ".trellis" / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + try: + from common.active_task import resolve_context_key # type: ignore[import-not-found] + except Exception: + return None + return resolve_context_key(hook_input, platform="codex") + + +def _resolve_active_task(trellis_dir: Path, hook_input: dict): + scripts_dir = trellis_dir / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_active_task # type: ignore[import-not-found] + + return resolve_active_task(trellis_dir.parent, hook_input, platform="codex") + + +def run_script(script_path: Path, context_key: str | None = None) -> str: + try: + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + if context_key: + env["TRELLIS_CONTEXT_ID"] = context_key + cmd = [sys.executable, "-W", "ignore", str(script_path)] + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=5, + cwd=str(script_path.parent.parent.parent), + env=env, + ) + return result.stdout if result.returncode == 0 else "No context available" + except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError): + return "No context available" + + +def _normalize_task_ref(task_ref: str) -> str: + normalized = task_ref.strip() + if not normalized: + return "" + + path_obj = Path(normalized) + if path_obj.is_absolute(): + return str(path_obj) + + normalized = normalized.replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + + if normalized.startswith("tasks/"): + return f".trellis/{normalized}" + + return normalized + + +def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path: + normalized = _normalize_task_ref(task_ref) + path_obj = Path(normalized) + if path_obj.is_absolute(): + return path_obj + if normalized.startswith(".trellis/"): + return trellis_dir.parent / path_obj + return trellis_dir / "tasks" / path_obj + + +def _get_task_status(trellis_dir: Path, hook_input: dict) -> str: + active = _resolve_active_task(trellis_dir, hook_input) + if not active.task_path: + return ( + "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}\n" + "Next: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish" + ) + + task_json_path = task_dir / "task.json" + task_data: dict = {} + if task_json_path.is_file(): + try: + task_data = json.loads(task_json_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, PermissionError): + pass + + task_title = task_data.get("title", task_ref) + task_status = task_data.get("status", "unknown") + + if task_status == "completed": + return ( + f"Status: COMPLETED\nTask: {task_title}\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: PLANNING\nTask: {task_title}\nPresent: {present_line}\n" + "Next: Load trellis-brainstorm and write prd.md. Stay in planning." + ) + + 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: {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 + 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 + + 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() + start: "int | None" = None + end: int = len(lines) + start_match = f"## {start_header}" + end_match = f"## {end_header}" + for i, line in enumerate(lines): + stripped = line.strip() + if start is None and stripped == start_match: + start = i + continue + if start is not None and stripped == end_match: + end = i + break + if start is None: + return "" + return "\n".join(lines[start:end]).rstrip() + + +_BREADCRUMB_TAG_RE = re.compile( + r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]", + re.DOTALL, +) + + +def _strip_breadcrumb_tag_blocks(content: str) -> str: + 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 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 - Session Summary", + "Full guide: .trellis/workflow.md. Step detail: `python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y>`.", + "", + ] + + phases = _extract_range(content, "Phase Index", "Phase 1: Plan") + if phases: + out_lines.append(_strip_breadcrumb_tag_blocks(phases).rstrip()) + + return "\n".join(out_lines).rstrip() + + +def main() -> None: + if should_skip_injection(): + sys.exit(0) + + # Read hook input from stdin + try: + hook_input = json.loads(sys.stdin.read()) + if not isinstance(hook_input, dict): + hook_input = {} + project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve() + except (json.JSONDecodeError, KeyError): + hook_input = {} + project_dir = Path(".").resolve() + + configure_project_encoding(project_dir) + + trellis_dir = project_dir / ".trellis" + spec_index_paths = _collect_spec_index_paths(trellis_dir) + + output = StringIO() + + output.write("""<session-context> +Trellis compact SessionStart context. Use it to orient the session; load details on demand. +</session-context> + +""") + output.write(FIRST_REPLY_NOTICE) + output.write("\n\n") + + output.write("<current-state>\n") + output.write(_build_compact_current_state(trellis_dir, hook_input, spec_index_paths)) + output.write("\n</current-state>\n\n") + + output.write("<trellis-workflow>\n") + output.write(_build_workflow_toc(trellis_dir / "workflow.md")) + output.write("\n</trellis-workflow>\n\n") + + output.write("<guidelines>\n") + output.write( + "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" + ) + + 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") + + output.write( + "Discover more via: " + "`python3 ./.trellis/scripts/get_context.py --mode packages`\n" + ) + output.write("</guidelines>\n\n") + + task_status = _get_task_status(trellis_dir, hook_input) + output.write(f"<task-status>\n{task_status}\n</task-status>\n\n") + + output.write("""<ready> +Context loaded. Follow <task-status>. Load workflow/spec/task details only when needed. +</ready>""") + + context = output.getvalue() + result = { + "suppressOutput": True, + "systemMessage": f"Trellis context injected ({len(context)} chars)", + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": context, + }, + } + + print(json.dumps(result, ensure_ascii=False), flush=True) + + +if __name__ == "__main__": + main() diff --git a/.cursor/agents/trellis-check.md b/.cursor/agents/trellis-check.md new file mode 100644 index 0000000..8fad81b --- /dev/null +++ b/.cursor/agents/trellis-check.md @@ -0,0 +1,114 @@ +--- +name: trellis-check +description: Trellis quality check agent. Use this exact agent for Trellis task verification, check.jsonl context injection, and self-fixing code review. Do not use generic/default/generalPurpose agents for Trellis checks. +tools: Read, Write, Edit, Bash, Glob, Grep +--- +# Check Agent + +You are the Check Agent in the Trellis workflow. + +## Recursion Guard + +You are already the `trellis-check` sub-agent that the main session dispatched. Do the review and fixes directly. + +- Do NOT spawn another `trellis-check` or `trellis-implement` sub-agent. +- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. +- Only the main session may dispatch Trellis implement/check agents. If more implementation work is needed, report that recommendation instead of spawning. + +## Trellis Context Loading Protocol + +Look for the `<!-- trellis-hook-injected -->` marker in your input above. + +- **If the marker is present**: task artifacts, spec, and research files have already been auto-loaded for you above. Proceed with the check work directly. +- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: <path>`, then Read `<task-path>/check.jsonl`, each listed file, `<task-path>/prd.md`, `<task-path>/design.md` if present, and `<task-path>/implement.md` if present before doing the work. + +## Context + +Before checking, read: +- `.trellis/spec/` - Development guidelines +- Task `prd.md` - Requirements document +- Task `design.md` - Technical design (if exists) +- Task `implement.md` - Execution plan (if exists) +- Pre-commit checklist for quality standards + +## Core Responsibilities + +1. **Get code changes** - Use git diff to get uncommitted code +2. **Review task artifacts** - Check changes against prd.md, design.md if present, and implement.md if present +3. **Check against specs** - Verify code follows guidelines +4. **Self-fix** - Fix issues yourself, not just report them +5. **Run verification** - typecheck and lint + +## Important + +**Fix issues yourself**, don't just report them. + +You have write and edit tools, you can modify code directly. + +--- + +## Workflow + +### Step 1: Get Changes + +```bash +git diff --name-only # List changed files +git diff # View specific changes +``` + +### Step 2: Check Against Specs and Task Artifacts + +Read the task's prd.md, design.md if present, and implement.md if present, then read relevant specs in `.trellis/spec/` to check code: + +- Does it satisfy the task requirements +- Does it follow the technical design and implementation plan when present +- Does it follow directory structure conventions +- Does it follow naming conventions +- Does it follow code patterns +- Are there missing types +- Are there potential bugs + +### Step 3: Self-Fix + +After finding issues: + +1. Fix the issue directly (use edit tool) +2. Record what was fixed +3. Continue checking other issues + +### Step 4: Run Verification + +Run project's lint and typecheck commands to verify changes. + +If failed, fix issues and re-run. + +--- + +## Report Format + +```markdown +## Self-Check Complete + +### Files Checked + +- src/components/Feature.tsx +- src/hooks/useFeature.ts + +### Issues Found and Fixed + +1. `<file>:<line>` - <what was fixed> +2. `<file>:<line>` - <what was fixed> + +### Issues Not Fixed + +(If there are issues that cannot be self-fixed, list them here with reasons) + +### Verification Results + +- TypeCheck: Passed +- Lint: Passed + +### Summary + +Checked X files, found Y issues, all fixed. +``` diff --git a/.cursor/agents/trellis-implement.md b/.cursor/agents/trellis-implement.md new file mode 100644 index 0000000..a5a734e --- /dev/null +++ b/.cursor/agents/trellis-implement.md @@ -0,0 +1,109 @@ +--- +name: trellis-implement +description: Trellis implementation agent. Use this exact agent for Trellis task implementation, implement.jsonl context injection, and hook-injection tests. Do not use generic/default/generalPurpose agents for Trellis implementation. No git commit allowed. +tools: Read, Write, Edit, Bash, Glob, Grep +--- +# Implement Agent + +You are the Implement Agent in the Trellis workflow. + +## Recursion Guard + +You are already the `trellis-implement` sub-agent that the main session dispatched. Do the implementation work directly. + +- Do NOT spawn another `trellis-implement` or `trellis-check` sub-agent. +- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. +- Only the main session may dispatch Trellis implement/check agents. If more parallel work is needed, report that recommendation instead of spawning. + +## Trellis Context Loading Protocol + +Look for the `<!-- trellis-hook-injected -->` marker in your input above. + +- **If the marker is present**: prd / spec / research files have already been auto-loaded for you above. Proceed with the implementation work directly. +- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: <path>`, then Read `<task-path>/implement.jsonl`, each listed file, `<task-path>/prd.md`, `<task-path>/design.md` if present, and `<task-path>/implement.md` if present before doing the work. + +## Context + +Before implementing, read: +- `.trellis/workflow.md` - Project workflow +- `.trellis/spec/` - Development guidelines +- Task `prd.md` - Requirements document +- Task `design.md` - Technical design (if exists) +- Task `implement.md` - Execution plan (if exists) + +## Core Responsibilities + +1. **Understand specs** - Read relevant spec files in `.trellis/spec/` +2. **Understand task artifacts** - Read prd.md, design.md if present, and implement.md if present +3. **Implement features** - Write code following specs and task artifacts +4. **Self-check** - Ensure code quality +5. **Report results** - Report completion status + +## Forbidden Operations + +**Do NOT execute these git commands:** + +- `git commit` +- `git push` +- `git merge` + +--- + +## Workflow + +### 1. Understand Specs + +Read relevant specs based on task type: + +- Spec layers: `.trellis/spec/<package>/<layer>/` +- Shared guides: `.trellis/spec/guides/` + +### 2. Understand Requirements + +Read the task's prd.md, design.md if present, and implement.md if present: + +- What are the core requirements +- Key points of technical design +- Implementation order, validation commands, and rollback points + +### 3. Implement Features + +- Write code following specs and task artifacts +- Follow existing code patterns +- Only do what's required, no over-engineering + +### 4. Verify + +Run project's lint and typecheck commands to verify changes. + +--- + +## Report Format + +```markdown +## Implementation Complete + +### Files Modified + +- `src/components/Feature.tsx` - New component +- `src/hooks/useFeature.ts` - New hook + +### Implementation Summary + +1. Created Feature component... +2. Added useFeature hook... + +### Verification Results + +- Lint: Passed +- TypeCheck: Passed +``` + +--- + +## Code Standards + +- Follow existing code patterns +- Don't add unnecessary abstractions +- Only do what's required, no over-engineering +- Keep code readable diff --git a/.cursor/agents/trellis-research.md b/.cursor/agents/trellis-research.md new file mode 100644 index 0000000..e9cc492 --- /dev/null +++ b/.cursor/agents/trellis-research.md @@ -0,0 +1,136 @@ +--- +name: trellis-research +description: Trellis research agent. Use this exact agent for Trellis task research and research/ persistence. Do not use generic/default/generalPurpose agents for Trellis research. +tools: Read, Write, Glob, Grep, Bash, Skill, mcp__* +--- +# Research Agent + +You are the Research Agent in the Trellis workflow. + +## Core Principle + +**You do one thing: find, explain, and PERSIST information.** + +Conversations get compacted; files don't. Every research output MUST end up as a file under `{TASK_DIR}/research/`. Returning findings only through the chat reply is a failure — the caller cannot read them next session. + +--- + +## Core Responsibilities + +1. **Internal Search** — locate files/components, understand code logic, discover patterns (Glob, Grep, Read) +2. **External Search** — library docs, API references, best practices (web search) +3. **Persist** — write each research topic to `{TASK_DIR}/research/<topic>.md` +4. **Report** — return file paths + one-line summaries to the main agent (not full content) + +--- + +## Workflow + +### Step 1: Resolve Current Task + +Run `python3 ./.trellis/scripts/task.py current --source` → active task path. If no active task is set, ask the user where to write output; do NOT guess. + +Ensure `{TASK_DIR}/research/` exists: + +```bash +mkdir -p <TASK_DIR>/research +``` + +### Step 2: Understand Search Request + +Classify: internal / external / mixed. Determine scope (global / specific directory) and expected shape (file list / pattern notes / tech comparison). + +### Step 3: Execute Search + +Run independent searches in parallel (Glob + Grep + web) for efficiency. + +### Step 4: Persist Each Topic + +For each distinct research topic, Write a markdown file at `{TASK_DIR}/research/<topic-slug>.md`. Use the File Format below. + +### Step 5: Report to Main Agent + +Reply with ONLY: + +- List of files written (paths relative to repo root) +- One-line summary per file +- Any critical caveats that the main agent needs to know right now + +Do NOT paste full research content into the reply. The files are the contract. + +--- + +## Scope Limits (Strict) + +### Write ALLOWED + +- `{TASK_DIR}/research/*.md` — your own output +- Creating `{TASK_DIR}/research/` if it doesn't exist (via `mkdir -p`) + +### Write FORBIDDEN + +- Code files (`src/`, `lib/`, …) +- Spec files (`.trellis/spec/`) — main agent should use `update-spec` skill instead +- `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.claude/`, `.cursor/`, etc.) +- Other task directories +- Any git operation (commit / push / branch / merge) + +If the user asks you to edit code, decline and suggest spawning `implement` instead. + +--- + +## File Format + +Each `{TASK_DIR}/research/<topic>.md` should follow: + +```markdown +# Research: <topic> + +- **Query**: <original query> +- **Scope**: <internal / external / mixed> +- **Date**: <YYYY-MM-DD> + +## Findings + +### Files Found + +| File Path | Description | +|---|---| +| `src/services/xxx.ts` | Main implementation | +| `src/types/xxx.ts` | Type definitions | + +### Code Patterns + +<describe patterns, cite file:line> + +### External References + +- [Library X docs](url) — <why relevant, version constraints> + +### Related Specs + +- `.trellis/spec/xxx.md` — <description> + +## Caveats / Not Found + +<anything incomplete or uncertain> +``` + +--- + +## Guidelines + +### DO + +- Provide specific file paths and line numbers +- Quote actual code snippets +- Persist every topic to its own file +- Return file paths in your reply, not the full content +- Mark "not found" explicitly when searches come up empty + +### DON'T + +- Don't write code or modify files outside `{TASK_DIR}/research/` +- Don't guess uncertain info +- Don't paste full research text into the reply (files are the deliverable) +- Don't propose improvements or critique implementation (that's not your role) diff --git a/.cursor/commands/trellis-continue.md b/.cursor/commands/trellis-continue.md new file mode 100644 index 0000000..1a76565 --- /dev/null +++ b/.cursor/commands/trellis-continue.md @@ -0,0 +1,56 @@ +# Continue Current Task + +Resume work on the current task — pick up at the right phase/step in `.trellis/workflow.md`. + +--- + +## Step 1: Load Current Context + +```bash +python3 ./.trellis/scripts/get_context.py +``` + +Confirms: current task, git state, recent commits. + +## Step 2: Load the Phase Index + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase +``` + +Shows the Phase Index (Plan / Execute / Finish) with routing + skill mapping. + +## Step 3: Decide Where You Are + +`get_context.py` shows the active task's `status` field. Route by `status` + artifact presence. 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` 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.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 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 + +Once you know which step to resume at: + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase --step <X.X> --platform cursor +``` + +Follow the loaded instructions. After each `[required]` step completes, move to the next. + +--- + +## Reference + +Full workflow and detailed phase steps live in `.trellis/workflow.md`. This command is only an entry point — the canonical guidance is there. diff --git a/.cursor/commands/trellis-finish-work.md b/.cursor/commands/trellis-finish-work.md new file mode 100644 index 0000000..54c2f67 --- /dev/null +++ b/.cursor/commands/trellis-finish-work.md @@ -0,0 +1,66 @@ +# Finish Work + +Wrap up the current session: archive the active task (and any other completed-but-unarchived tasks the user wants to clean up) and record the session journal. Code commits are NOT done here — those happen in workflow Phase 3.4 before you invoke this command. + +## Step 1: Survey current state + +```bash +python3 ./.trellis/scripts/get_context.py --mode record +``` + +This prints: + +- **My active tasks** — review whether any besides the current one are actually done (code merged, AC met) and should be archived this round. +- **Git status** — quick visual on what's dirty. +- **Recent commits** — you'll need their hashes in Step 4 for `--commit`. + +If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless. + +## Step 2: Sanity check — classify dirty paths + +Run: + +```bash +git status --porcelain +``` + +Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this skill's own work. + +For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics: + +- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task +- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task +- Paths in unrelated areas you have no recollection of touching this session → other parallel work + +Then route: + +- **Any remaining path looks like current-task work** — bail out with: + > "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running `/trellis-finish-work`." + + Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there. +- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3: + > "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`." +- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer. + +## Step 3: Archive task(s) + +```bash +python3 ./.trellis/scripts/task.py archive <task-name> +``` + +At minimum: the current active task (if any). Plus any extra tasks the user confirmed in Step 1. Each archive produces a `chore(task): archive ...` commit via the script's auto-commit. + +If there is no active task and the user did not confirm any cleanup archives, skip this step. + +## Step 4: Record session journal + +```bash +python3 ./.trellis/scripts/add_session.py \ + --title "Session Title" \ + --commit "hash1,hash2" \ + --summary "Brief summary" +``` + +Use the work-commit hashes produced in Phase 3.4 (visible in Step 1's `Recent commits` list, or via `git log --oneline`) for `--commit`. Do not include the archive commit hashes from Step 3. This produces a `chore: record journal` commit. + +Final git log order: `<work commits from 3.4>` → `chore(task): archive ...` (one or more) → `chore: record journal`. diff --git a/.cursor/hooks.json b/.cursor/hooks.json new file mode 100644 index 0000000..1fdf55e --- /dev/null +++ b/.cursor/hooks.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "hooks": { + "preToolUse": [ + { + "command": "python3 .cursor/hooks/inject-subagent-context.py", + "matcher": "Task|Subagent", + "timeout": 30 + } + ], + "sessionStart": [ + { + "command": "python3 .cursor/hooks/session-start.py", + "timeout": 30 + } + ], + "beforeShellExecution": [ + { + "command": "python3 .cursor/hooks/inject-shell-session-context.py", + "timeout": 5 + } + ] + } +} diff --git a/.cursor/hooks/inject-shell-session-context.py b/.cursor/hooks/inject-shell-session-context.py new file mode 100644 index 0000000..8d497f7 --- /dev/null +++ b/.cursor/hooks/inject-shell-session-context.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Cursor beforeShellExecution hook: bridge conversation identity to task.py. + +Cursor's shell command environment does not inherit SessionStart data. This +hook writes a short-lived runtime ticket before Cursor runs a shell command +that calls `task.py start/current/finish`. The task script then consumes the +ticket only when it has no native session environment. +""" +from __future__ import annotations + +import hashlib +import json +import os +import shlex +import sys +import time +from pathlib import Path +from typing import Any + + +DIR_WORKFLOW = ".trellis" +DIR_RUNTIME = ".runtime" +DIR_CURSOR_SHELL = "cursor-shell" +SESSION_SUBCOMMANDS = {"start", "current", "finish"} +TICKET_TTL_SECONDS = 30 +CONTEXT_IDENTITY_KEYS = ( + "session_id", + "sessionId", + "sessionID", + "conversation_id", + "conversationId", + "conversationID", + "transcript_path", + "transcriptPath", + "transcript", +) + + +def _string_value(value: Any) -> str | None: + if isinstance(value, str): + stripped = value.strip() + return stripped or None + return None + + +def _find_trellis_root(start: Path) -> Path | None: + current = start.resolve() + while True: + if (current / DIR_WORKFLOW).is_dir(): + return current + if current == current.parent: + return None + current = current.parent + + +def _runtime_ticket_dir(root: Path) -> Path: + return root / DIR_WORKFLOW / DIR_RUNTIME / DIR_CURSOR_SHELL + + +def _load_active_task_resolver(root: Path): + scripts_dir = root / DIR_WORKFLOW / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_context_key # type: ignore[import-not-found] + + return resolve_context_key + + +def _extract_task_subcommands(command: str) -> list[dict[str, str]]: + try: + tokens = shlex.split(command, posix=os.name != "nt") + except ValueError: + return [] + + subcommands: list[dict[str, str]] = [] + for index, token in enumerate(tokens[:-1]): + if Path(token.strip("\"'")).name != "task.py": + continue + name = tokens[index + 1] + if name not in SESSION_SUBCOMMANDS: + continue + item = {"name": name} + if name == "start" and index + 2 < len(tokens): + item["task_ref"] = tokens[index + 2] + subcommands.append(item) + return subcommands + + +def _cleanup_expired_tickets(ticket_dir: Path, now: float) -> None: + if not ticket_dir.is_dir(): + return + for ticket_path in ticket_dir.glob("*.json"): + try: + data = json.loads(ticket_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + continue + expires_at = data.get("expires_at_epoch") + if isinstance(expires_at, (int, float)) and expires_at < now: + try: + ticket_path.unlink() + except OSError: + pass + + +def _has_context_identity(hook_input: dict[str, Any]) -> bool: + return any(_string_value(hook_input.get(key)) for key in CONTEXT_IDENTITY_KEYS) + + +def _write_ticket( + root: Path, + hook_input: dict[str, Any], + context_key: str, + subcommands: list[dict[str, str]], +) -> None: + now = time.time() + ticket_dir = _runtime_ticket_dir(root) + ticket_dir.mkdir(parents=True, exist_ok=True) + _cleanup_expired_tickets(ticket_dir, now) + + command = _string_value(hook_input.get("command")) or "" + digest = hashlib.sha256( + f"{context_key}\0{command}\0{now}".encode("utf-8"), + ).hexdigest()[:16] + ticket_path = ticket_dir / f"{int(now * 1000)}-{digest}.json" + + payload = { + "platform": "cursor", + "context_key": context_key, + "conversation_id": _string_value(hook_input.get("conversation_id")), + "session_id": _string_value(hook_input.get("session_id")), + "generation_id": _string_value(hook_input.get("generation_id")), + "cwd": _string_value(hook_input.get("cwd")), + "command": command, + "subcommands": subcommands, + "created_at_epoch": now, + "expires_at_epoch": now + TICKET_TTL_SECONDS, + } + ticket_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + + +def main() -> int: + if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": + return 0 + + try: + hook_input = json.loads(sys.stdin.read()) + except (json.JSONDecodeError, ValueError): + hook_input = {} + if not isinstance(hook_input, dict): + hook_input = {} + + command = _string_value(hook_input.get("command")) or "" + subcommands = _extract_task_subcommands(command) + if not subcommands: + return 0 + + cwd = Path(_string_value(hook_input.get("cwd")) or os.getcwd()) + root = _find_trellis_root(cwd) + if root is None: + return 0 + + if not _has_context_identity(hook_input): + return 0 + + resolve_context_key = _load_active_task_resolver(root) + context_key = resolve_context_key(hook_input, platform="cursor") + if not context_key: + return 0 + + try: + _write_ticket(root, hook_input, context_key, subcommands) + except OSError: + return 0 + + print(json.dumps({"permission": "allow"}, ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.cursor/hooks/inject-subagent-context.py b/.cursor/hooks/inject-subagent-context.py new file mode 100644 index 0000000..975babc --- /dev/null +++ b/.cursor/hooks/inject-subagent-context.py @@ -0,0 +1,771 @@ +#!/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 isinstance(input_data.get("cursor_version"), str): + return "cursor" + env_map = { + "CLAUDE_PROJECT_DIR": "claude", + "CURSOR_PROJECT_DIR": "cursor", + "CODEBUDDY_PROJECT_DIR": "codebuddy", + "FACTORY_PROJECT_DIR": "droid", + "GEMINI_PROJECT_DIR": "gemini", + "QODER_PROJECT_DIR": "qoder", + "KIRO_PROJECT_DIR": "kiro", + "COPILOT_PROJECT_DIR": "copilot", + } + for env_name, platform in env_map.items(): + if os.environ.get(env_name): + return platform + script_parts = set(Path(sys.argv[0]).parts) + if ".claude" in script_parts: + return "claude" + if ".cursor" in script_parts: + return "cursor" + if ".gemini" in script_parts: + return "gemini" + if ".qoder" in script_parts: + return "qoder" + if ".codebuddy" in script_parts: + return "codebuddy" + if ".factory" in script_parts: + return "droid" + if ".kiro" in script_parts: + return "kiro" + return None + + +def get_current_task(repo_root: str, input_data: dict) -> str | None: + """Resolve current task directory through the unified active task resolver.""" + scripts_dir = Path(repo_root) / DIR_WORKFLOW / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + try: + from common.active_task import resolve_active_task # type: ignore[import-not-found] + except Exception: + return None + + active = resolve_active_task( + Path(repo_root), + input_data, + platform=_detect_platform(input_data), + ) + return active.task_path + + +def read_file_content(base_path: str, file_path: str) -> str | None: + """Read file content, return None if file doesn't exist""" + full_path = os.path.join(base_path, file_path) + if os.path.exists(full_path) and os.path.isfile(full_path): + try: + with open(full_path, "r", encoding="utf-8") as f: + return f.read() + except Exception: + return None + return None + + +def read_directory_contents( + base_path: str, dir_path: str, max_files: int = 20 +) -> list[tuple[str, str]]: + """ + Read all .md files in a directory + + Args: + base_path: Base path (usually repo_root) + dir_path: Directory relative path + max_files: Max files to read (prevent huge directories) + + Returns: + [(file_path, content), ...] + """ + full_path = os.path.join(base_path, dir_path) + if not os.path.exists(full_path) or not os.path.isdir(full_path): + return [] + + results = [] + try: + # Only read .md files, sorted by filename + md_files = sorted( + [ + f + for f in os.listdir(full_path) + if f.endswith(".md") and os.path.isfile(os.path.join(full_path, f)) + ] + ) + + for filename in md_files[:max_files]: + file_full_path = os.path.join(full_path, filename) + relative_path = os.path.join(dir_path, filename) + try: + with open(file_full_path, "r", encoding="utf-8") as f: + content = f.read() + results.append((relative_path, content)) + except Exception: + continue + except Exception: + pass + + return results + + +def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]: + """ + Read all file/directory contents referenced in jsonl file + + Schema: + {"file": "path/to/file.md", "reason": "..."} + {"file": "path/to/dir/", "type": "directory", "reason": "..."} + {"_example": "..."} # seed row — skipped (no `file` field) + + Rows without a ``file`` field (e.g. the self-describing seed line written + by ``task.py create`` before the agent has curated entries) are skipped + silently. If the resulting entry list is empty, a stderr warning is + emitted so the operator can debug missing context. + + Returns: + [(path, content), ...] + """ + full_path = os.path.join(base_path, jsonl_path) + if not os.path.exists(full_path): + print( + f"[inject-subagent-context] WARN: {jsonl_path} not found — " + f"sub-agent will receive only task artifacts", + file=sys.stderr, + ) + return [] + + results = [] + saw_real_entry = False + try: + with open(full_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + item = json.loads(line) + file_path = item.get("file") or item.get("path") + entry_type = item.get("type", "file") + + if not file_path: + # Seed / comment row — skip silently + continue + + saw_real_entry = True + if entry_type == "directory": + # Read all .md files in directory + dir_contents = read_directory_contents(base_path, file_path) + results.extend(dir_contents) + else: + # Read single file + content = read_file_content(base_path, file_path) + if content: + results.append((file_path, content)) + except json.JSONDecodeError: + continue + except Exception: + pass + + if not saw_real_entry: + print( + f"[inject-subagent-context] WARN: {jsonl_path} has no curated " + f"entries (only seed / empty) — sub-agent will receive only " + f"task artifacts. See workflow.md planning artifact guidance.", + file=sys.stderr, + ) + + return results + + + + +def get_agent_context(repo_root: str, task_dir: str, agent_type: str) -> str: + """ + Get context from {agent_type}.jsonl for the specified agent. + Only reads implement.jsonl or check.jsonl (the two JSONL files the task system creates). + """ + context_parts = [] + + agent_jsonl = f"{task_dir}/{agent_type}.jsonl" + for file_path, content in read_jsonl_entries(repo_root, agent_jsonl): + context_parts.append(f"=== {file_path} ===\n{content}") + + return "\n\n".join(context_parts) + + +def get_implement_context(repo_root: str, task_dir: str) -> str: + """ + Complete context for Implement Agent + + Read order: + 1. All files in implement.jsonl (spec/research manifests) + 2. prd.md (requirements) + 3. design.md if present (technical design) + 4. implement.md if present (execution plan) + """ + context_parts = [] + + # 1. Read implement.jsonl + base_context = get_agent_context(repo_root, task_dir, "implement") + if base_context: + context_parts.append(base_context) + + # 2. Requirements document + prd_content = read_file_content(repo_root, f"{task_dir}/prd.md") + if prd_content: + context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}") + + # 3. Technical design for complex tasks + design_content = read_file_content(repo_root, f"{task_dir}/design.md") + if design_content: + context_parts.append( + f"=== {task_dir}/design.md (Technical Design) ===\n{design_content}" + ) + + # 4. Execution plan for complex tasks + implement_plan_content = read_file_content(repo_root, f"{task_dir}/implement.md") + if implement_plan_content: + context_parts.append( + f"=== {task_dir}/implement.md (Execution Plan) ===\n{implement_plan_content}" + ) + + 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. + """ + context_parts = [] + + for file_path, content in read_jsonl_entries(repo_root, f"{task_dir}/check.jsonl"): + context_parts.append(f"=== {file_path} ===\n{content}") + + prd_content = read_file_content(repo_root, f"{task_dir}/prd.md") + if prd_content: + context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}") + + design_content = read_file_content(repo_root, f"{task_dir}/design.md") + if design_content: + context_parts.append( + f"=== {task_dir}/design.md (Technical Design) ===\n{design_content}" + ) + + implement_plan_content = read_file_content(repo_root, f"{task_dir}/implement.md") + if implement_plan_content: + context_parts.append( + f"=== {task_dir}/implement.md (Execution Plan) ===\n{implement_plan_content}" + ) + + return "\n\n".join(context_parts) + + +def get_finish_context(repo_root: str, task_dir: str) -> str: + """ + Context for Finish phase: reuses check.jsonl + prd.md + (Finish is a final check, same context source.) + """ + return get_check_context(repo_root, task_dir) + + + +def build_implement_prompt(original_prompt: str, context: str) -> str: + """Build complete prompt for Implement""" + return f"""<!-- trellis-hook-injected --> +# Implement Agent Task + +You are the Implement Agent in the Multi-Agent Pipeline. + +## Your Context + +All the information you need has been prepared for you: + +{context} + +--- + +## Your Task + +{original_prompt} + +--- + +## Workflow + +1. **Understand specs** - All dev specs are injected above, understand them + 2. **Understand 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 _extract_subagent_name(value: Any) -> str: + """Extract a sub-agent name from common platform encodings. + + Cursor's native Task args encode custom sub-agents as a protobuf oneof, + which can appear in hook JSON as either ``{"custom": {"name": "..."}}`` + or ``{"type": {"case": "custom", "value": {"name": "..."}}}``. + """ + direct = _string_value(value) + if direct: + return direct + + if not isinstance(value, dict): + return "" + + for key in ("name", "subagent_type_name", "subagentTypeName"): + direct = _string_value(value.get(key)) + if direct: + return direct + + custom = value.get("custom") + if isinstance(custom, dict): + custom_name = _string_value(custom.get("name")) + if custom_name: + return custom_name + + oneof = value.get("type") + if isinstance(oneof, dict): + case_name = _string_value(oneof.get("case")) + if case_name == "custom": + nested_value = oneof.get("value") + if isinstance(nested_value, dict): + custom_name = _string_value(nested_value.get("name")) + if custom_name: + return custom_name + if case_name: + return case_name + + case_name = _string_value(value.get("case")) + if case_name == "custom": + nested_value = value.get("value") + if isinstance(nested_value, dict): + custom_name = _string_value(nested_value.get("name")) + if custom_name: + return custom_name + if case_name: + return case_name + + for agent_name in AGENTS_ALL: + if agent_name in value: + return agent_name + + return "" + + +def _extract_subagent_type(tool_input: dict) -> str: + for key in ( + "subagent_type", + "subagentType", + "subagent_type_name", + "subagentTypeName", + "agent_type", + "agentType", + "name", + ): + agent_name = _extract_subagent_name(tool_input.get(key)) + if agent_name: + return agent_name + return "" + + +def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]: + """Parse hook input across different platform formats. + + Returns (subagent_type, original_prompt, tool_input). + Handles: + - Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type + - Cursor: tool_name=Task|Subagent, tool_input.subagent_type + - Copilot CLI: toolName=task (camelCase key, lowercase value) + - Gemini CLI: tool_name IS the agent name (BeforeTool matcher already filtered) + - Kiro: agentSpawn hook, agent_name field at top level + """ + tool_input = input_data.get("tool_input", {}) + + # Standard format: Task/Agent tool with subagent_type + tool_name = input_data.get("tool_name", "") or input_data.get("toolName", "") + if tool_name.lower() in ("task", "agent", "subagent"): + return ( + _extract_subagent_type(tool_input), + tool_input.get("prompt", ""), + tool_input, + ) + + # Kiro: agentSpawn hook passes agent_name at top level + agent_name = input_data.get("agent_name", "") + if agent_name: + return agent_name, tool_input.get("prompt", input_data.get("prompt", "")), tool_input + + # Gemini CLI: BeforeTool where tool_name IS the agent name + # (matcher already ensured it's one of our agents) + if tool_name in AGENTS_ALL: + return tool_name, tool_input.get("prompt", ""), tool_input + + # Copilot CLI: toolName field (camelCase), value might be the agent name + tool_name_camel = input_data.get("toolName", "") + if tool_name_camel in AGENTS_ALL: + return tool_name_camel, input_data.get("toolArgs", ""), tool_input + + return "", "", tool_input + + +def main(): + if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": + sys.exit(0) + + try: + input_data = json.load(sys.stdin) + except json.JSONDecodeError: + sys.exit(0) + + subagent_type, original_prompt, tool_input = _parse_hook_input(input_data) + cwd = input_data.get("cwd", os.getcwd()) + + # Only handle subagent types we care about + if subagent_type not in AGENTS_ALL: + sys.exit(0) + + # Find repo root + repo_root = find_repo_root(cwd) + if not repo_root: + sys.exit(0) + + # Get current task directory (research doesn't require it) + task_dir = get_current_task(repo_root, input_data) + + # implement/check need task directory + if subagent_type in AGENTS_REQUIRE_TASK: + if not task_dir: + sys.exit(0) + # Check if task directory exists + task_dir_full = os.path.join(repo_root, task_dir) + if not os.path.exists(task_dir_full): + sys.exit(0) + + # Check for [finish] marker in prompt (check agent with finish context) + is_finish_phase = "[finish]" in original_prompt.lower() + + # Get context and build prompt based on subagent type + if subagent_type == AGENT_IMPLEMENT: + assert task_dir is not None # validated above + context = get_implement_context(repo_root, task_dir) + new_prompt = build_implement_prompt(original_prompt, context) + elif subagent_type == AGENT_CHECK: + assert task_dir is not None # validated above + if is_finish_phase: + # Finish phase: use finish context (lighter, focused on final verification) + context = get_finish_context(repo_root, task_dir) + new_prompt = build_finish_prompt(original_prompt, context) + else: + # Regular check phase: use check context (full specs for self-fix loop) + context = get_check_context(repo_root, task_dir) + new_prompt = build_check_prompt(original_prompt, context) + elif subagent_type == AGENT_RESEARCH: + # Research can work without task directory + context = get_research_context(repo_root, task_dir) + new_prompt = build_research_prompt(original_prompt, context) + else: + sys.exit(0) + + if not context: + sys.exit(0) + + # Return updated input — use a multi-format output that covers all platforms. + # Most platforms ignore unrecognized fields, so we include multiple formats. + # The platform picks whichever fields it understands. + updated = {**tool_input, "prompt": new_prompt} + output = { + # Claude Code / Qoder / CodeBuddy / Droid format + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow", + "updatedInput": updated, + }, + # Cursor format + "permission": "allow", + "updated_input": updated, + # Gemini format + "updatedInput": updated, + } + + print(json.dumps(output, ensure_ascii=False)) + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/.cursor/hooks/session-start.py b/.cursor/hooks/session-start.py new file mode 100644 index 0000000..a7f28bf --- /dev/null +++ b/.cursor/hooks/session-start.py @@ -0,0 +1,844 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Session Start Hook - Inject structured context +""" +from __future__ import annotations + +# IMPORTANT: Suppress all warnings FIRST +import warnings +warnings.filterwarnings("ignore") + +import json +import os +import re +import shlex +import subprocess +import sys +from io import StringIO +from pathlib import Path + + +def _normalize_windows_shell_path(path_str: str) -> str: + """Normalize Unix-style shell paths to real Windows paths. + + On Windows, shells like Git Bash / MSYS2 / Cygwin may report paths like + `/d/Users/...` or `/cygdrive/d/Users/...`. `Path.resolve()` will misinterpret + these as `D:/d/Users...` on drive D: (or similar), breaking repo root + detection. + + This function is intentionally conservative: it only rewrites patterns that + unambiguously represent a drive letter mount. + """ + if not isinstance(path_str, str) or not path_str: + return path_str + + # Only relevant on Windows; keep other platforms untouched. + if not sys.platform.startswith("win"): + return path_str + + p = path_str.strip() + + # Already a Windows drive path (C:\... or C:/...) + if re.match(r"^[A-Za-z]:[\/]", p): + return p + + # MSYS/Git-Bash style: /c/Users/... or /d/Work/... + m = re.match(r"^/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + # Cygwin style: /cygdrive/c/Users/... + m = re.match(r"^/cygdrive/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + # WSL mounted drive (sometimes leaked into env): /mnt/c/Users/... + m = re.match(r"^/mnt/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + return path_str + + +FIRST_REPLY_NOTICE = """<first-reply-notice> +First visible reply: say once in Chinese that Trellis SessionStart context is loaded, then answer directly. +This notice is one-shot: do not repeat it after the first assistant reply in the same session. +</first-reply-notice>""" + +# Force UTF-8 on stdin/stdout/stderr on Windows. Default codepage there is +# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets) +# both in stdin (hook payload from host CLI) and stdout (our emitted blocks) +# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8` +# but applied per-stream so we don't depend on host CLI's command wiring. +if sys.platform.startswith("win"): + import io as _io + for _stream_name in ("stdin", "stdout", "stderr"): + _stream = getattr(sys, _stream_name, None) + if _stream is None: + continue + if hasattr(_stream, "reconfigure"): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] + except Exception: + pass + elif hasattr(_stream, "detach"): + try: + setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) + except Exception: + pass + + + +def _has_curated_jsonl_entry(jsonl_path: Path) -> bool: + """Return True iff jsonl has at least one row with a ``file`` field. + + A freshly seeded jsonl only contains a ``{"_example": ...}`` row (no + ``file`` key) — that is NOT "ready". Readiness requires at least one + curated entry. Matches the contract used by hook-inject and pull-based + sub-agent context loaders. + """ + try: + for line in jsonl_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + row = json.loads(line) + except json.JSONDecodeError: + continue + if isinstance(row, dict) and row.get("file"): + return True + except (OSError, UnicodeDecodeError): + return False + return False + + +def should_skip_injection() -> bool: + """Check if any platform's non-interactive flag is set, or if Trellis + hooks are explicitly disabled via TRELLIS_HOOKS=0 / TRELLIS_DISABLE_HOOKS=1. + """ + if os.environ.get("TRELLIS_HOOKS") == "0": + return True + if os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": + return True + non_interactive_vars = [ + "CLAUDE_NON_INTERACTIVE", + "QODER_NON_INTERACTIVE", + "CODEBUDDY_NON_INTERACTIVE", + "FACTORY_NON_INTERACTIVE", + "CURSOR_NON_INTERACTIVE", + "GEMINI_NON_INTERACTIVE", + "KIRO_NON_INTERACTIVE", + "COPILOT_NON_INTERACTIVE", + "TRAE_NON_INTERACTIVE", + ] + return any(os.environ.get(var) == "1" for var in non_interactive_vars) + + +def read_file(path: Path, fallback: str = "") -> str: + try: + return path.read_text(encoding="utf-8") + except (FileNotFoundError, PermissionError): + return fallback + + +def _repo_relative(repo_root: Path, path: Path) -> str: + try: + return path.relative_to(repo_root).as_posix() + except ValueError: + return str(path) + + +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 _detect_platform(input_data: dict) -> str | None: + if isinstance(input_data.get("cursor_version"), str): + return "cursor" + env_map = { + "CLAUDE_PROJECT_DIR": "claude", + "CURSOR_PROJECT_DIR": "cursor", + "CODEBUDDY_PROJECT_DIR": "codebuddy", + "FACTORY_PROJECT_DIR": "droid", + "GEMINI_PROJECT_DIR": "gemini", + "QODER_PROJECT_DIR": "qoder", + "KIRO_PROJECT_DIR": "kiro", + "COPILOT_PROJECT_DIR": "copilot", + "TRAE_PROJECT_DIR": "trae", + } + for env_name, platform in env_map.items(): + if os.environ.get(env_name): + return platform + script_parts = set(Path(sys.argv[0]).parts) + if ".claude" in script_parts: + return "claude" + if ".cursor" in script_parts: + return "cursor" + if ".codex" in script_parts: + return "codex" + if ".gemini" in script_parts: + return "gemini" + if ".qoder" in script_parts: + return "qoder" + if ".codebuddy" in script_parts: + return "codebuddy" + if ".factory" in script_parts: + return "droid" + if ".kiro" in script_parts: + return "kiro" + if ".trae" in script_parts: + return "trae" + return None + + +def _resolve_context_key(trellis_dir: Path, input_data: dict) -> str | None: + scripts_dir = trellis_dir / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_context_key # type: ignore[import-not-found] + + return resolve_context_key(input_data, platform=_detect_platform(input_data)) + + +def _persist_context_key_for_bash(context_key: str | None) -> None: + """Expose Trellis session identity to later Claude Code Bash commands. + + Claude Code SessionStart hooks can append exports to CLAUDE_ENV_FILE; those + variables are then available to Bash tools in the same conversation. Without + this bridge, `task.py start` has hook stdin during SessionStart but no + session identity when the AI later runs it as a normal shell command. + """ + if not context_key: + return + env_file = os.environ.get("CLAUDE_ENV_FILE") + if not env_file: + return + try: + with open(env_file, "a", encoding="utf-8") as handle: + handle.write(f"export TRELLIS_CONTEXT_ID={shlex.quote(context_key)}\n") + except OSError: + pass + + +def _resolve_active_task(trellis_dir: Path, input_data: dict): + scripts_dir = trellis_dir / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_active_task # type: ignore[import-not-found] + + return resolve_active_task( + trellis_dir.parent, + input_data, + platform=_detect_platform(input_data), + ) + + +def run_script(script_path: Path, context_key: str | None = None) -> str: + try: + if script_path.suffix == ".py": + # Add PYTHONIOENCODING to force UTF-8 in subprocess + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + if context_key: + env["TRELLIS_CONTEXT_ID"] = context_key + cmd = [sys.executable, "-W", "ignore", str(script_path)] + else: + env = os.environ.copy() + if context_key: + env["TRELLIS_CONTEXT_ID"] = context_key + cmd = [str(script_path)] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=5, + cwd=script_path.parent.parent.parent, + env=env, + ) + return result.stdout if result.returncode == 0 else "No context available" + except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError): + return "No context available" + + +def _normalize_task_ref(task_ref: str) -> str: + normalized = task_ref.strip() + if not normalized: + return "" + + path_obj = Path(normalized) + if path_obj.is_absolute(): + return str(path_obj) + + normalized = normalized.replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + + if normalized.startswith("tasks/"): + return f".trellis/{normalized}" + + return normalized + + +def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path: + normalized = _normalize_task_ref(task_ref) + path_obj = Path(normalized) + if path_obj.is_absolute(): + return path_obj + if normalized.startswith(".trellis/"): + return trellis_dir.parent / path_obj + return trellis_dir / "tasks" / path_obj + + +def _get_task_status(trellis_dir: Path, input_data: dict) -> str: + """Return compact active-task status, artifact presence, and next action.""" + active = _resolve_active_task(trellis_dir, input_data) + + if not active.task_path: + return ( + "Status: NO ACTIVE TASK\n" + "Next-Action: Classify the current turn before creating any Trellis task. " + "Simple conversation / small task asks only whether this turn should create a Trellis task. " + "Complex task asks whether task creation and planning are allowed." + ) + + task_ref = active.task_path + task_dir = _resolve_task_dir(trellis_dir, task_ref) + if active.stale or not task_dir.is_dir(): + return ( + f"Status: STALE POINTER\nTask: {task_ref}\n" + f"Next-Action: Run `python3 ./.trellis/scripts/task.py finish` to clear the stale pointer, " + "then ask the user what to work on next." + ) + + task_json_path = task_dir / "task.json" + task_data = {} + if task_json_path.is_file(): + try: + task_data = json.loads(task_json_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, PermissionError): + pass + + task_title = task_data.get("title", task_ref) + task_status = task_data.get("status", "unknown") + artifact_names = ("prd.md", "design.md", "implement.md", "implement.jsonl", "check.jsonl") + present = [name for name in artifact_names if (task_dir / name).is_file()] + if (task_dir / "research").is_dir(): + present.append("research/") + present_line = ", ".join(present) if present else "(none)" + + if task_status == "completed": + return ( + f"Status: COMPLETED\nTask: {task_title}\n" + f"Present: {present_line}\n" + "Next-Action: Run `/trellis:finish-work`. If the working tree is dirty, return to Phase 3.4 first." + ) + + has_prd = (task_dir / "prd.md").is_file() + has_design = (task_dir / "design.md").is_file() + has_implement_plan = (task_dir / "implement.md").is_file() + implement_jsonl = task_dir / "implement.jsonl" + check_jsonl = task_dir / "check.jsonl" + jsonl_ready = ( + (not implement_jsonl.is_file() or _has_curated_jsonl_entry(implement_jsonl)) + and (not check_jsonl.is_file() or _has_curated_jsonl_entry(check_jsonl)) + ) + + if task_status == "planning" and not has_prd: + return ( + f"Status: PLANNING\nTask: {task_title}\n" + f"Present: {present_line}\n" + "Next-Action: Load `trellis-brainstorm` and write `prd.md`. Stay in planning." + ) + + if task_status == "planning": + missing_complex = [ + name for name, exists in ( + ("design.md", has_design), + ("implement.md", has_implement_plan), + ) + if not exists + ] + next_bits: list[str] = [] + if missing_complex: + next_bits.append( + "Lightweight task can request start review with PRD-only; " + f"complex task must add {', '.join(missing_complex)} before start" + ) + else: + next_bits.append("Planning artifacts are present; ask for review before `task.py start`") + if not jsonl_ready: + next_bits.append("curate `implement.jsonl` and `check.jsonl` before sub-agent mode start") + return ( + f"Status: PLANNING\nTask: {task_title}\n" + f"Present: {present_line}\n" + f"Next-Action: {'; '.join(next_bits)}. Do not enter implementation until the user confirms start." + ) + + return ( + f"Status: {str(task_status).upper()}\nTask: {task_title}\n" + f"Present: {present_line}\n" + "Next-Action: Follow the matching per-turn workflow-state. " + "Implementation/check context order is jsonl entries -> `prd.md` -> `design.md if present` -> `implement.md if present`." + ) + + +def _load_trellis_config(trellis_dir: Path, input_data: dict) -> tuple: + """Load Trellis config for session-start decisions. + + Returns: + (is_mono, packages_dict, spec_scope, task_pkg, default_pkg) + """ + scripts_dir = trellis_dir / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + + try: + from common.config import get_default_package, get_packages, get_spec_scope, is_monorepo # type: ignore[import-not-found] + from common.paths import get_current_task # type: ignore[import-not-found] + + repo_root = trellis_dir.parent + is_mono = is_monorepo(repo_root) + packages = get_packages(repo_root) or {} + scope = get_spec_scope(repo_root) + + # Get active task's package + task_pkg = None + current = get_current_task( + repo_root, + input_data, + platform=_detect_platform(input_data), + ) + if current: + task_json = repo_root / current / "task.json" + if task_json.is_file(): + try: + data = json.loads(task_json.read_text(encoding="utf-8")) + if isinstance(data, dict): + tp = data.get("package") + if isinstance(tp, str) and tp: + task_pkg = tp + except (json.JSONDecodeError, OSError): + pass + + default_pkg = get_default_package(repo_root) + return is_mono, packages, scope, task_pkg, default_pkg + except Exception: + return False, {}, None, None, None + + +def _check_legacy_spec(trellis_dir: Path, is_mono: bool, packages: dict) -> str | None: + """Check for legacy spec directory structure in monorepo. + + Returns warning message if legacy structure detected, None otherwise. + """ + if not is_mono or not packages: + return None + + spec_dir = trellis_dir / "spec" + if not spec_dir.is_dir(): + return None + + # Check for legacy flat spec dirs (spec/backend/, spec/frontend/ with index.md) + has_legacy = False + for legacy_name in ("backend", "frontend"): + legacy_dir = spec_dir / legacy_name + if legacy_dir.is_dir() and (legacy_dir / "index.md").is_file(): + has_legacy = True + break + + if not has_legacy: + return None + + # Check which packages are missing spec/<pkg>/ directory + missing = [ + name for name in sorted(packages.keys()) + if not (spec_dir / name).is_dir() + ] + + if not missing: + return None # All packages have spec dirs + + if len(missing) == len(packages): + return ( + f"[!] Legacy spec structure detected: found `spec/backend/` or `spec/frontend/` " + f"but no package-scoped `spec/<package>/` directories.\n" + f"Monorepo packages: {', '.join(sorted(packages.keys()))}\n" + f"Please reorganize: `spec/backend/` -> `spec/<package>/backend/`" + ) + return ( + f"[!] Partial spec migration detected: packages {', '.join(missing)} " + f"still missing `spec/<pkg>/` directory.\n" + f"Please complete migration for all packages." + ) + + +def _resolve_spec_scope( + is_mono: bool, + packages: dict, + scope, + task_pkg: str | None, + default_pkg: str | None, +) -> set | None: + """Resolve which packages should have their specs injected. + + Returns: + Set of package names to include, or None for full scan. + """ + if not is_mono or not packages: + return None # Single-repo: full scan + + if scope is None: + return None # No scope configured: full scan + + if isinstance(scope, str) and scope == "active_task": + if task_pkg and task_pkg in packages: + return {task_pkg} + if default_pkg and default_pkg in packages: + return {default_pkg} + return None # Fallback to full scan + + if isinstance(scope, list): + valid = set() + for entry in scope: + if entry in packages: + valid.add(entry) + else: + print( + f"Warning: spec_scope contains unknown package: {entry}, ignoring", + file=sys.stderr, + ) + + if valid: + # Warn if active task is out of scope + if task_pkg and task_pkg not in valid: + print( + f"Warning: active task package '{task_pkg}' is out of configured spec_scope", + file=sys.stderr, + ) + return valid + + # All entries invalid: fallback chain + print( + "Warning: all spec_scope entries invalid, falling back to task/default/full", + file=sys.stderr, + ) + if task_pkg and task_pkg in packages: + return {task_pkg} + if default_pkg and default_pkg in packages: + return {default_pkg} + return None # Full scan + + return None # Unknown scope type: full scan + + +def _collect_spec_index_paths(trellis_dir: Path, allowed_pkgs: set | None) -> 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 + + if allowed_pkgs is not None and sub.name not in allowed_pkgs: + continue + for nested in sorted(sub.iterdir()): + if not nested.is_dir(): + continue + nested_index = nested / "index.md" + if nested_index.is_file(): + paths.append(f".trellis/spec/{sub.name}/{nested.name}/index.md") + + return paths + + +def _build_compact_current_state( + trellis_dir: Path, + input_data: 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, input_data) + 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 + 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 + + 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`. + + Both parameters are full header lines WITHOUT the `## ` prefix (e.g. "Phase Index"). + Returns empty string if start header is not found. + End header missing → extracts to end of file. + """ + lines = content.splitlines() + start: int | None = None + end: int = len(lines) + start_match = f"## {start_header}" + end_match = f"## {end_header}" + for i, line in enumerate(lines): + stripped = line.strip() + if start is None and stripped == start_match: + start = i + continue + if start is not None and stripped == end_match: + end = i + break + if start is None: + return "" + return "\n".join(lines[start:end]).rstrip() + + +_BREADCRUMB_TAG_RE = re.compile( + r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]", + re.DOTALL, +) + + +def _strip_breadcrumb_tag_blocks(content: str) -> str: + """Remove `[workflow-state:STATUS]...[/workflow-state:STATUS]` blocks. + + The tag blocks live inside `## Phase Index` (since v0.5.0-rc.0, when + they were colocated with their phase summaries) and are consumed by the + UserPromptSubmit hook (`inject-workflow-state.py`). The session-start + payload already covers the full step bodies, so re-inlining the + breadcrumbs here would just duplicate context. + """ + 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_overview(workflow_path: Path) -> str: + """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 - Session Summary", + "Full guide: .trellis/workflow.md. Step detail: `python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y>`.", + "", + ] + + phases = _extract_range(content, "Phase Index", "Phase 1: Plan") + if phases: + out_lines.append(_strip_breadcrumb_tag_blocks(phases).rstrip()) + + return "\n".join(out_lines).rstrip() + + +def main(): + if should_skip_injection(): + sys.exit(0) + + try: + hook_input = json.loads(sys.stdin.read()) + if not isinstance(hook_input, dict): + hook_input = {} + except (json.JSONDecodeError, ValueError): + hook_input = {} + + # Try platform-specific env vars, hook cwd, fallback to cwd + project_dir_env_vars = [ + "CLAUDE_PROJECT_DIR", + "QODER_PROJECT_DIR", + "CODEBUDDY_PROJECT_DIR", + "FACTORY_PROJECT_DIR", + "CURSOR_PROJECT_DIR", + "GEMINI_PROJECT_DIR", + "KIRO_PROJECT_DIR", + "COPILOT_PROJECT_DIR", + "TRAE_PROJECT_DIR", + ] + project_dir = None + for var in project_dir_env_vars: + val = os.environ.get(var) + if val: + project_dir = Path(_normalize_windows_shell_path(val)).resolve() + break + if project_dir is None: + project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve() + + trellis_dir = project_dir / ".trellis" + context_key = _resolve_context_key(trellis_dir, hook_input) + _persist_context_key_for_bash(context_key) + + # Load config for scope filtering and legacy detection + is_mono, packages, scope_config, task_pkg, default_pkg = _load_trellis_config( + trellis_dir, + hook_input, + ) + allowed_pkgs = _resolve_spec_scope(is_mono, packages, scope_config, task_pkg, default_pkg) + + output = StringIO() + + spec_index_paths = _collect_spec_index_paths(trellis_dir, allowed_pkgs) + + output.write("""<session-context> +Trellis compact SessionStart context. Use it to orient the session; load details on demand. +</session-context> + +""") + output.write(FIRST_REPLY_NOTICE) + output.write("\n\n") + + # Legacy migration warning + legacy_warning = _check_legacy_spec(trellis_dir, is_mono, packages) + if legacy_warning: + output.write(f"<migration-warning>\n{legacy_warning}\n</migration-warning>\n\n") + + output.write("<current-state>\n") + output.write(_build_compact_current_state(trellis_dir, hook_input, spec_index_paths)) + output.write("\n</current-state>\n\n") + + output.write("<trellis-workflow>\n") + output.write(_build_workflow_overview(trellis_dir / "workflow.md")) + output.write("\n</trellis-workflow>\n\n") + + output.write("<guidelines>\n") + output.write( + "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" + ) + + 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") + + output.write( + "Discover more via: " + "`python3 ./.trellis/scripts/get_context.py --mode packages`\n" + ) + output.write("</guidelines>\n\n") + + # Check task status and inject structured tag + task_status = _get_task_status(trellis_dir, hook_input) + output.write(f"<task-status>\n{task_status}\n</task-status>\n\n") + + output.write("""<ready> +Context loaded. Follow <task-status>. Load workflow/spec/task details only when needed. +</ready>""") + + context_text = output.getvalue() + + # Kiro (CLI trellis agent agentSpawn) adds a hook's stdout directly to the + # conversation context — no JSON envelope. Emit the bare overview text. + # Conditionally isolated: all other platforms keep the JSON path below. + if _detect_platform(hook_input) == "kiro": + print(context_text, flush=True) + return + + result = { + # Claude Code / Qoder / CodeBuddy / Droid / Gemini / Copilot format + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": context_text, + }, + # Cursor sessionStart format (top-level snake_case per Cursor docs) + "additional_context": context_text, + } + + # Output JSON - stdout is already configured for UTF-8 + print(json.dumps(result, ensure_ascii=False), flush=True) + + +if __name__ == "__main__": + main() diff --git a/.cursor/skills/trellis-before-dev/SKILL.md b/.cursor/skills/trellis-before-dev/SKILL.md new file mode 100644 index 0000000..5a4b852 --- /dev/null +++ b/.cursor/skills/trellis-before-dev/SKILL.md @@ -0,0 +1,40 @@ +--- +name: trellis-before-dev +description: "Discovers and injects project-specific coding guidelines from .trellis/spec/ before implementation begins. Reads spec indexes, pre-development checklists, and shared thinking guides for the target package. Use when starting a new coding task, before writing any code, switching to a different package, or needing to refresh project conventions and standards." +--- + +Read the relevant development guidelines before starting your task. + +Execute these steps: + +1. **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 + ``` + +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 + +4. **Read the spec index** for each relevant module: + ```bash + cat .trellis/spec/<package>/<layer>/index.md + ``` + Follow the **"Pre-Development Checklist"** section in the index. + +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. + +6. **Always read shared guides**: + ```bash + cat .trellis/spec/guides/index.md + ``` + +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/.cursor/skills/trellis-brainstorm/SKILL.md b/.cursor/skills/trellis-brainstorm/SKILL.md new file mode 100644 index 0000000..806ee31 --- /dev/null +++ b/.cursor/skills/trellis-brainstorm/SKILL.md @@ -0,0 +1,173 @@ +--- +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." +--- + +# Trellis Brainstorm + +## Non-Negotiable Interview Contract + +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. + +Ask the questions one at a time. + +## 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. + +Do not ask the user to confirm facts that the repository can answer. Ask only for product intent, preference, scope, risk tolerance, or decisions that remain ambiguous after inspection. + +--- + +Use this skill during Phase 1 planning to turn the user's request into clear requirements and planning artifacts. + +## Preconditions + +Use this skill only after task-creation consent has been given and the user is ready to enter Trellis planning. + +If no task exists yet, create one: + +```bash +TASK_DIR=$(python3 ./.trellis/scripts/task.py create "<short task title>" --slug <slug>) +``` + +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. + +`task.py create` creates the default `prd.md`. Update that file with the current understanding before asking follow-up questions. + +## Planning Flow + +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. Ask the single highest-value remaining question. +5. Include your recommended answer with the question. +6. After each user answer, update `prd.md` before continuing. +7. For complex tasks, create or update `design.md` and `implement.md` before implementation starts. +8. Before final review or `task.py start`, run the PRD convergence pass below. + +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. + +## Question Rules + +Ask only one question per message. + +Each question must include: + +- the decision needed +- why the answer matters +- your recommended answer +- the trade-off if the user chooses differently + +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. + +## 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. + +### Step 1: Restate the Problem + +Strip away implementation details to one sentence. + +> 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 + +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 + +For each component of the current plan: + +- **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. + +### Step 4: Build Up from Truths + +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?" + +### Step 5: Validate + +- Does the solution solve the original problem? +- What assumptions need verification? +- What's the simplest experiment to test this? + +## Artifact Rules + +`prd.md` records requirements and acceptance: + +- goal and user value +- confirmed facts +- requirements +- acceptance criteria +- out of scope +- open questions that still block planning + +`design.md` records technical design for complex tasks: + +- architecture and boundaries +- data flow and contracts +- compatibility and migration notes +- important trade-offs +- operational or rollback considerations + +`implement.md` records execution planning for complex tasks: + +- ordered implementation checklist +- validation commands +- risky files or rollback points +- follow-up checks before `task.py start` + +Lightweight tasks may have only `prd.md`. Complex tasks must have `prd.md`, `design.md`, and `implement.md` before `task.py start`. + +`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`. + +## PRD Convergence Pass + +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. + +The pass must be lossless: + +- 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. +- Keep only genuinely blocking open questions. + +After the pass, read `prd.md` top to bottom and verify that no fact is repeated across sections unless the repetition adds new information. + +## Quality Bar + +Before declaring planning ready: + +- `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. +- Remaining open questions are genuinely about user intent or scope. +- 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 user has reviewed the final planning artifacts or explicitly approved proceeding. + +Do not start implementation until the user approves or asks for implementation. diff --git a/.cursor/skills/trellis-break-loop/SKILL.md b/.cursor/skills/trellis-break-loop/SKILL.md new file mode 100644 index 0000000..1c8b397 --- /dev/null +++ b/.cursor/skills/trellis-break-loop/SKILL.md @@ -0,0 +1,188 @@ +--- +name: trellis-break-loop +description: "Deep bug analysis to break the fix-forget-repeat cycle. Analyzes root cause category, why fixes failed, prevention mechanisms, and captures knowledge into specs. Use after fixing a bug to prevent the same class of bugs." +--- + +# Break the Loop - Deep Bug Analysis + +When debug is complete, use this for deep analysis to break the "fix bug -> forget -> repeat" cycle. + +--- + +## Analysis Framework + +Analyze the bug you just fixed from these 5 dimensions: + +### 1. Root Cause Category + +Which category does this bug belong to? + +| Category | Characteristics | Example | +|----------|-----------------|---------| +| **A. Missing Spec** | No documentation on how to do it | New feature without checklist | +| **B. Cross-Layer Contract** | Interface between layers unclear | API returns different format than expected | +| **C. Change Propagation Failure** | Changed one place, missed others | Changed function signature, missed call sites | +| **D. Test Coverage Gap** | Unit test passes, integration fails | Works alone, breaks when combined | +| **E. Implicit Assumption** | Code relies on undocumented assumption | Timestamp seconds vs milliseconds | + +### 2. Why Fixes Failed (if applicable) + +If you tried multiple fixes before succeeding, analyze each failure: + +- **Surface Fix**: Fixed symptom, not root cause +- **Incomplete Scope**: Found root cause, didn't cover all cases +- **Tool Limitation**: grep missed it, type check wasn't strict +- **Mental Model**: Kept looking in same layer, didn't think cross-layer + +### 3. Prevention Mechanisms + +What mechanisms would prevent this from happening again? + +| Type | Description | Example | +|------|-------------|---------| +| **Documentation** | Write it down so people know | Update thinking guide | +| **Architecture** | Make the error impossible structurally | Type-safe wrappers | +| **Compile-time** | Strict type checking, no escape hatches | Signature change causes compile error | +| **Runtime** | Monitoring, alerts, scans | Detect orphan entities | +| **Test Coverage** | E2E tests, integration tests | Verify full flow | +| **Code Review** | Checklist, PR template | "Did you check X?" | + +### 4. Systematic Expansion + +What broader problems does this bug reveal? + +- **Similar Issues**: Where else might this problem exist? +- **Design Flaw**: Is there a fundamental architecture issue? +- **Process Flaw**: Is there a development process improvement? +- **Knowledge Gap**: Is the team missing some understanding? + +### 5. Knowledge Capture + +Solidify insights into the system: + +- [ ] Update `.trellis/spec/guides/` thinking guides +- [ ] Update relevant `.trellis/spec/` docs +- [ ] Create issue record (if applicable) +- [ ] Create feature ticket for root fix +- [ ] Update check guidelines if needed + +--- + +## Output Format + +Please output analysis in this format: + +```markdown +## Bug Analysis: [Short Description] + +### 1. Root Cause Category +- **Category**: [A/B/C/D/E] - [Category Name] +- **Specific Cause**: [Detailed description] + +### 2. Why Fixes Failed (if applicable) +1. [First attempt]: [Why it failed] +2. [Second attempt]: [Why it failed] +... + +### 3. Prevention Mechanisms +| Priority | Mechanism | Specific Action | Status | +|----------|-----------|-----------------|--------| +| P0 | ... | ... | TODO/DONE | + +### 4. Systematic Expansion +- **Similar Issues**: [List places with similar problems] +- **Design Improvement**: [Architecture-level suggestions] +- **Process Improvement**: [Development process suggestions] + +### 5. Knowledge Capture +- [ ] [Documents to update / tickets to create] +``` + +--- + +## Core Philosophy + +> **The value of debugging is not in fixing the bug, but in making this class of bugs never happen again.** + +Three levels of insight: +1. **Tactical**: How to fix THIS bug +2. **Strategic**: How to prevent THIS CLASS of bugs +3. **Philosophical**: How to expand thinking patterns + +30 minutes of analysis saves 30 hours of future debugging. + +## 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 + +**IMPORTANT**: After completing the analysis above, you MUST immediately: + +1. **Update spec/guides** - Don't just list TODOs, actually update the relevant files: + - If it's a cross-platform issue → update `cross-platform-thinking-guide.md` + - If it's a cross-layer issue → update `cross-layer-thinking-guide.md` + - If it's a code reuse issue → update `code-reuse-thinking-guide.md` + - If it's domain-specific → update `backend/*.md` or `frontend/*.md` + +2. **Sync templates** - After updating `.trellis/spec/`, sync to `src/templates/markdown/spec/` + +3. **Commit the spec updates** - This is the primary output, not just the analysis text + +> **The analysis is worthless if it stays in chat. The value is in the updated specs.** diff --git a/.cursor/skills/trellis-channel/SKILL.md b/.cursor/skills/trellis-channel/SKILL.md new file mode 100644 index 0000000..511ee02 --- /dev/null +++ b/.cursor/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 <board> --scope global +trellis channel thread <board> <thread> --scope global +trellis channel context list <board> --scope global --thread <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 <my_signal>` 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/.cursor/skills/trellis-channel/references/command-reference.md b/.cursor/skills/trellis-channel/references/command-reference.md new file mode 100644 index 0000000..75def26 --- /dev/null +++ b/.cursor/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 <project|global>` unless noted; `project` +is the default and resolves against the current cwd's project bucket. + +## Top-level + +``` +trellis channel <subcommand> +``` + +> Multi-agent collaboration runtime — spawn / coordinate / interrupt worker +> agents through a shared event log. + +--- + +## Create / List + +### `create <name>` + +```bash +trellis channel create <name> + [--scope project|global] # default: project + [--type chat|forum] # default: chat + [--task <path>] # associated Trellis task dir + [--project <slug>] + [--labels a,b,c] + [--description <text>] # stable channel description + [--context-file <abs-path>] ... # repeatable + [--context-raw <text>] ... # repeatable + [--linked-context-file <abs-path>] # [deprecated alias] + [--linked-context-raw <text>] # [deprecated alias] + [--cwd <path>] # recorded in create event + [--by <agent>] # 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 <slug>] # 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 <name> [text]` + +```bash +trellis channel send <name> [text] + --as <agent> # REQUIRED — author + [--scope project|global] + [--to <agents,csv>] # default: broadcast + [--stdin | --text-file <path>] # 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 <name>` + +```bash +trellis channel messages <name> + [--scope project|global] + [--raw] # one JSON event per line + [--follow] # stream new events + [--last <N>] # last N matching events + [--since <seq>] # seq > N + [--kind <kind>] # one of CHANNEL_EVENT_KINDS + [--from <csv>] # author filter + [--to <target>] # routing target filter + [--thread <key>] # forum-only + [--action <thread-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 <name>` + +```bash +trellis channel wait <name> + --as <agent> # REQUIRED — self for filter ctx + [--scope project|global] + [--timeout <Ns|Nm|Nh|Nms>] # parsed by parseDuration + [--from <a,b>] # author CSV + [--kind <k1,k2>] # CSV, OR semantics + [--thread <key>] # forum filter + [--action <thread-action>] # forum filter + [--to <target>] # 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 '<x>'. 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 <name> [text]` + +```bash +trellis channel interrupt <name> [text] + --as <agent> # REQUIRED — caller + --to <agent> # REQUIRED — target worker + [--scope project|global] + [--stdin | --text-file <path>] +``` + +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 <name>` + +```bash +trellis channel spawn <name> + [--scope project|global] + [--agent <agent-name>] # loads .trellis/agents/<name>.md + [--provider claude|codex] # overrides agent file + [--as <worker-name>] # default: agent name + [--cwd <path>] + [--model <id>] + [--resume <id>] # session/thread id resume + [--timeout <Ns|Nm|Nh>] # auto-kill after duration + [--warn-before <Ns|Nm|Nh>] # supervisor_warning lead time + # default 5m, 0ms disables + [--file <path>] ... # glob, repeatable; inject content + [--jsonl <path>] ... # Trellis manifest, repeatable + [--by <agent>] # spawn-event author + # default: TRELLIS_CHANNEL_AS env or 'main' + [--inbox-policy explicitOnly|broadcastAndExplicit] + # default explicitOnly + [--idle-timeout <Ns|Nm|Nh>] # OOM-guard idle TTL + # default 5m, 0 disables + [--max-live-workers <n>] # 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 <worker>`. +- 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 <name>] + [--provider claude|codex] + [--as <worker-name>] + [--cwd <path>] + [--model <id>] + [--file <path>] ... # repeatable, glob + [--jsonl <path>] ... # repeatable + [--message <text> | --message-file <path> | --stdin] + [--timeout <Ns|Nm|Nh>] # default 5m +``` + +Behavior: +- One-shot. Auto-generates `run-<hex>` 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 <name>` + +```bash +trellis channel kill <name> + --as <agent> # 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 <name>` + +```bash +trellis channel rm <name> + [--scope project|global] +``` + +Behavior: +- Kills any live workers, then deletes the entire channel directory. +- Prints `Removed channel '<name>'`. + +### `prune` + +```bash +trellis channel prune + [--scope project|global] # omitted: scan every project + [--all | --empty | --idle <Ns|Nm|Nh|Nd> | --ephemeral] # mutually exclusive + [--yes] # actually delete (default: dry-run) + [--dry-run] # default true; redundant with default + [--keep <names,csv>] # 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 <name> <action>` + +```bash +trellis channel post <name> <action> + --as <agent> # REQUIRED + [--scope project|global] + [--thread <key>] # required except action=opened + [--title <text>] + [--text <text> | --stdin | --text-file <path>] + [--description <text>] # stable thread description + [--status <status>] + [--labels a,b] # REPLACES thread labels + [--assignees a,b] # REPLACES assignees + [--summary <text>] + [--context-file <abs-path>] ... + [--context-raw <text>] ... + [--linked-context-file <abs-path>] # [deprecated alias] + [--linked-context-raw <text>] # [deprecated alias] +``` + +Behavior: +- `<action>` 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 <name>` + +```bash +trellis channel forum <name> + [--scope project|global] + [--status <status>] + [--raw] +``` + +Behavior: +- Lists threads (reduced state). `--status` filters by current thread + status. `--raw` prints one JSON per thread. + +### `thread <name> <thread>` / `thread rename` + +```bash +trellis channel thread <name> <thread-key> + [--scope project|global] + [--raw] + +trellis channel thread rename <name> <old-thread> <new-thread> + --as <agent> # REQUIRED + [--scope project|global] +``` + +Behavior: +- `thread <name> <key>` shows one thread's timeline: + header `<thread> [<status>] <title>`, 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/.cursor/skills/trellis-channel/references/forum.md b/.cursor/skills/trellis-channel/references/forum.md new file mode 100644 index 0000000..06b7f36 --- /dev/null +++ b/.cursor/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/.cursor/skills/trellis-channel/references/progress-debugging.md b/.cursor/skills/trellis-channel/references/progress-debugging.md new file mode 100644 index 0000000..3ed40d6 --- /dev/null +++ b/.cursor/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/.cursor/skills/trellis-channel/references/workers.md b/.cursor/skills/trellis-channel/references/workers.md new file mode 100644 index 0000000..bcec98f --- /dev/null +++ b/.cursor/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/.cursor/skills/trellis-channel/references/workflows.md b/.cursor/skills/trellis-channel/references/workflows.md new file mode 100644 index 0000000..3319764 --- /dev/null +++ b/.cursor/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/.cursor/skills/trellis-check/SKILL.md b/.cursor/skills/trellis-check/SKILL.md new file mode 100644 index 0000000..c695abd --- /dev/null +++ b/.cursor/skills/trellis-check/SKILL.md @@ -0,0 +1,98 @@ +--- +name: trellis-check +description: "Comprehensive quality verification: spec compliance, lint, type-check, tests, cross-layer data flow, code reuse, and consistency checks. Use when code is written and needs quality verification, before committing changes, or to catch context drift during long sessions." +--- + +# Code Quality Check + +Comprehensive quality verification for recently written code. Combines spec compliance, cross-layer safety, and pre-commit checks. + +--- + +## Step 1: Identify What Changed + +```bash +git diff --name-only HEAD +git status +``` + +## Step 2: Read 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 +``` + +For each changed package/layer, read the spec index and follow its **Quality Check** section: + +```bash +cat .trellis/spec/<package>/<layer>/index.md +``` + +Read the specific guideline files referenced — the index is a pointer, not the goal. + +## Step 3: Run Project Checks + +Run the project's lint, type-check, and test commands. Fix any failures before proceeding. + +## Step 4: Review Against Checklist + +### Code Quality + +- [ ] Linter passes? +- [ ] Type checker passes (if applicable)? +- [ ] Tests pass? +- [ ] No debug logging left in? +- [ ] No suppressed warnings or type-safety bypasses? + +### Test Coverage + +- [ ] New function → unit test added? +- [ ] Bug fix → regression test added? +- [ ] Changed behavior → existing tests updated? + +### Spec Sync + +- [ ] Does `.trellis/spec/` need updates? (new patterns, conventions, lessons learned) + +> "If I fixed a bug or discovered something non-obvious, should I document it so future me won't hit the same issue?" → If YES, update the relevant spec doc. + +## Step 5: Cross-Layer Dimensions (if applicable) + +Skip this step if your change is confined to a single layer. + +### A. Data Flow (changes touch 3+ layers) + +- [ ] Read flow traces correctly: Storage → Service → API → UI +- [ ] Write flow traces correctly: UI → API → Service → Storage +- [ ] Types/schemas correctly passed between layers? +- [ ] Errors properly propagated to caller? + +### B. Code Reuse (modifying constants, creating utilities) + +- [ ] Searched for existing similar code before creating new? + ```bash + grep -r "pattern" src/ + ``` +- [ ] If 2+ places define same value → extracted to shared constant? +- [ ] After batch modification, all occurrences updated? + +### C. Import/Dependency (creating new files) + +- [ ] Correct import paths (relative vs absolute)? +- [ ] No circular dependencies? + +### D. Same-Layer Consistency + +- [ ] Other places using the same concept are consistent? + +--- + +## Step 6: Report and Fix + +Report violations found and fix them directly. Re-run project checks after fixes. diff --git a/.cursor/skills/trellis-meta/SKILL.md b/.cursor/skills/trellis-meta/SKILL.md new file mode 100644 index 0000000..0754b53 --- /dev/null +++ b/.cursor/skills/trellis-meta/SKILL.md @@ -0,0 +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, 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, bundled runtime agents, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.reasonix/`, `.kilocode/`, `.agent/`, `.devin/`, 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. +- 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` — 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 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 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 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 15 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, 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; 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` 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/.cursor/skills/trellis-meta/references/customize-local/add-project-local-conventions.md b/.cursor/skills/trellis-meta/references/customize-local/add-project-local-conventions.md new file mode 100644 index 0000000..608aaa6 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/customize-local/add-project-local-conventions.md @@ -0,0 +1,83 @@ +# Add Project-Local Conventions + +Often the user does not need to change Trellis mechanics; they need local AI to understand their team's conventions. In that case, prefer `.trellis/spec/` or a project-local skill instead of editing `trellis-meta`. + +## Where To Put Things + +| Content type | Location | +| --- | --- | +| Rules code must follow | `.trellis/spec/<layer>/` | +| Cross-layer thinking methods | `.trellis/spec/guides/` | +| AI capability for a project-specific flow | Platform-local skill | +| One-off task material | `.trellis/tasks/<task>/` | +| Session summary | `.trellis/workspace/<developer>/journal-N.md` | + +## Create A Project-Local Skill + +If the user wants AI to know "how this project customizes Trellis," create a local skill: + +```text +.claude/skills/trellis-local/ +└── SKILL.md +``` + +Example: + +```md +--- +name: trellis-local +description: "Project-local Trellis customizations for this repository. Use when changing this project's Trellis workflow, hooks, local agents, or team-specific conventions." +--- + +# Trellis Local + +## Local Scope + +This skill documents this repository's Trellis customizations only. + +## Custom Workflow Rules + +- ... + +## Local Hook Changes + +- ... + +## Local Agent Changes + +- ... +``` + +For multi-platform projects, place equivalent versions in other platform skill directories, or use `.agents/skills/` for platforms that support the shared layer. + +## Write To `.trellis/spec/` + +If the content is a coding convention, write it to spec. Examples: + +```text +.trellis/spec/backend/error-handling.md +.trellis/spec/frontend/components.md +.trellis/spec/guides/cross-platform-thinking-guide.md +``` + +After writing it, update the corresponding `index.md` so AI can find the new rule from the entry point. + +## Make The Current Task Use New Conventions + +After writing a spec, add it to the current task context: + +```bash +python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/backend/error-handling.md" "Error handling conventions" +python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/backend/error-handling.md" "Review error handling" +``` + +## Do Not Store Project-Private Rules In `trellis-meta` + +`trellis-meta` is a public skill for understanding Trellis architecture and local customization entry points. Put project-private content in: + +- `.trellis/spec/` +- a project-local skill +- the current task +- workspace journal + +This prevents future updates to Trellis's built-in `trellis-meta` from overwriting the team's own conventions. diff --git a/.cursor/skills/trellis-meta/references/customize-local/change-agents.md b/.cursor/skills/trellis-meta/references/customize-local/change-agents.md new file mode 100644 index 0000000..88e7376 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/customize-local/change-agents.md @@ -0,0 +1,56 @@ +# Change Local Agents + +When the user wants to change `trellis-research`, `trellis-implement`, or `trellis-check` behavior, edit platform agent files in the user project. + +## Read These Files First + +1. Target platform agent directory +2. `.trellis/workflow.md` Phase 2 / research routing +3. Current task `prd.md` +4. Current task `implement.jsonl` / `check.jsonl` +5. Relevant hook or agent prelude + +## Common Paths + +| Platform | Path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/cli/agents/trellis-*.md` | + +Use the actual paths in the user project as authoritative. + +## Common Needs + +| Need | Which agent to edit | +| --- | --- | +| Research must write files, not only reply in chat | `trellis-research` | +| Certain local specs must be read before implementation | `trellis-implement` + `implement.jsonl` configuration rules | +| Specific commands must run during checking | `trellis-check` | +| Agent must not modify certain directories | The corresponding agent's write boundary instructions | +| Agent output format must be fixed | The corresponding agent's final/reporting instructions | + +## Modification Principles + +1. **Preserve role boundaries**: research investigates and persists; implement writes implementation; check reviews and fixes. +2. **Do not hard-code project specs into agents**: long-term specs belong in `.trellis/spec/`; agents are responsible for reading them. +3. **Make read order explicit**: active task -> PRD -> info -> JSONL -> spec/research. +4. **Make write boundaries explicit**: which directories may be written and which may not. +5. **Synchronize across platforms**: when the user configured multiple platforms, decide whether to change only the current platform or all platform agents. + +## Agent Pull Platforms + +If an agent file contains a prelude for "read task/context after startup," do not remove those steps when editing. Otherwise the agent will work only from chat context and bypass Trellis's core mechanism. + +## Hook Push Platforms + +If context is injected by a hook, the agent file should still retain responsibility boundaries. Do not remove PRD/spec requirements from the agent just because a hook injects context. diff --git a/.cursor/skills/trellis-meta/references/customize-local/change-context-loading.md b/.cursor/skills/trellis-meta/references/customize-local/change-context-loading.md new file mode 100644 index 0000000..002a259 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/customize-local/change-context-loading.md @@ -0,0 +1,84 @@ +# Change Local Context Loading + +Context loading determines when AI reads workflow, task, spec, research, workspace, and git status. Read this page when the user says "AI does not know the current task," "the agent did not read specs," or "there is too much/too little context." + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/scripts/get_context.py` +3. `.trellis/scripts/common/session_context.py` +4. `.trellis/scripts/common/task_context.py` +5. `.trellis/scripts/common/active_task.py` +6. Current platform hooks or agent files +7. The current task's `implement.jsonl` / `check.jsonl` + +## Context Sources + +| Source | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow and next-action hints. | +| `.trellis/tasks/<task>/prd.md` | Current task requirements. | +| `.trellis/tasks/<task>/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. | +| `.trellis/workspace/` | Session records. | +| git status | Current working tree changes. | + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Inject more/less information in new sessions | `session_context.py` or the platform `session-start` hook. | +| Change hints on each user input | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The `inject-workflow-state` hook is parser-only and reads the block verbatim. | +| Agent did not read specs | Task JSONL, agent prelude, `inject-subagent-context` hook. | +| Active task is lost | `active_task.py` and platform session identity propagation. | +| Change JSONL validation rules | `task_context.py`. | + +## JSONL Rules + +`implement.jsonl` / `check.jsonl` are the key context loading interface: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-x/research/api.md", "reason": "API research"} +``` + +Include only spec/research files. Do not put code files that will be modified into these manifests; agents read code files themselves during implementation. + +## Change Session Context + +If the user wants every new session to see more project state, edit: + +- `.trellis/scripts/common/session_context.py` +- the corresponding platform `session-start` hook + +Context cannot grow without bound. Prefer injecting indexes and paths so the AI can read detailed files on demand. + +## Change Sub-Agent Context + +First determine which mode the platform uses: + +- hook push: edit the `inject-subagent-context` hook. +- agent pull: edit the read steps in the corresponding `trellis-implement` / `trellis-check` agent file. + +In both modes, make sure the agent ultimately reads: + +1. active task +2. 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 + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py list-context <task> +python3 ./.trellis/scripts/task.py validate <task> +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Confirm the task and JSONL are correct before editing hooks/agents. diff --git a/.cursor/skills/trellis-meta/references/customize-local/change-hooks.md b/.cursor/skills/trellis-meta/references/customize-local/change-hooks.md new file mode 100644 index 0000000..79aa5c5 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/customize-local/change-hooks.md @@ -0,0 +1,57 @@ +# Change Local Hooks + +Hooks are the automation layer that connects a platform to Trellis. When the user wants to change "when context is injected," "how shell commands inherit a session," or "which files are read before an agent starts," hooks are usually the edit point. + +## Read These Files First + +1. Target platform settings/config, such as `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`, `.trae/hooks.json` +2. Target platform hooks directory +3. `.trellis/scripts/common/active_task.py` +4. `.trellis/scripts/common/session_context.py` +5. `.trellis/workflow.md` + +## Common Hook Types + +| Hook | Purpose | +| --- | --- | +| session-start | Injects a Trellis overview when a session starts, clears, or compacts. | +| workflow-state | Injects a state hint on each user input. | +| sub-agent context | Injects PRD/spec/research before an agent starts. | +| shell session bridge | Lets `task.py` commands in shell see the same session identity. | + +## Modification Steps + +1. Find the hook registration in settings/config. +2. Confirm the registered script path exists. +3. Read the hook script and identify inputs, outputs, and called `.trellis/scripts/`. +4. Modify hook behavior. +5. If the hook depends on workflow content, synchronize `.trellis/workflow.md`. + +## Example: Change New-Session Injection Content + +First find the session-start hook: + +```text +.claude/settings.json +.claude/hooks/session-start.py +``` + +If the hook ultimately calls `.trellis/scripts/get_context.py` or `session_context.py`, editing the local script is usually more robust than hard-coding content in the hook. + +## Example: Agent Did Not Read JSONL + +First confirm: + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py validate <task> +``` + +If the task and JSONL are correct, determine whether the platform uses hook push or agent pull. For hook push, edit `inject-subagent-context`; for agent pull, edit the agent file. + +## Notes + +- Settings handle registration, hook scripts handle behavior; inspect both together. +- Different platforms support different hook events. Do not directly copy another platform's settings. +- Hooks should read project-local `.trellis/`; they should not depend on Trellis upstream source paths. +- Hook failures should produce visible errors so AI does not silently lose context. diff --git a/.cursor/skills/trellis-meta/references/customize-local/change-skills-or-commands.md b/.cursor/skills/trellis-meta/references/customize-local/change-skills-or-commands.md new file mode 100644 index 0000000..0ed8792 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/customize-local/change-skills-or-commands.md @@ -0,0 +1,123 @@ +# Change Local Skills, Commands, Prompts, And Workflows + +When the user wants to change AI entry points, auto-trigger rules, or explicit command behavior, edit skills, commands, prompts, or workflows in local platform directories. + +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 + +| Goal | Recommendation | +| --- | --- | +| AI should automatically know a capability | Add or modify a skill. | +| User wants to trigger manually with a command | Add or modify a command/prompt/workflow. | +| Team project conventions | Prefer `.trellis/spec/` or a project-local skill — 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 + +A skill is usually: + +```text +<skill-name>/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should be short and responsible for triggering/routing. Put long content in `references/` so AI can read it on demand. + +The frontmatter description should specify when to use the skill. Example: + +```yaml +description: "Use when customizing this project's deployment workflow and release checklist." +``` + +Do not write vague descriptions such as "helpful project skill"; they can trigger incorrectly. + +### 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: + +- How the user triggers it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +If a command only repeats workflow rules, prefer making it reference/read `.trellis/workflow.md` instead of maintaining a second copy of the flow. + +## Common Paths + +| Platform | Entry directories | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| 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/` | +| Factory Droid | `.factory/skills/`, `.factory/commands/` | +| Pi Agent | `.pi/skills/` | +| Reasonix | `.reasonix/skills/` (no separate commands dir; slash commands built into the platform) | +| ZCode | `.agents/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 — 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 (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/.cursor/skills/trellis-meta/references/customize-local/change-spec-structure.md b/.cursor/skills/trellis-meta/references/customize-local/change-spec-structure.md new file mode 100644 index 0000000..ee9a176 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/customize-local/change-spec-structure.md @@ -0,0 +1,83 @@ +# Change Local Spec Structure + +When the user wants to change the engineering conventions AI follows, add new spec layers, or adjust monorepo package mapping, edit `.trellis/spec/` and `.trellis/config.yaml`. + +## Read These Files First + +1. `.trellis/config.yaml` +2. `.trellis/spec/` +3. `.trellis/workflow.md` planning artifact guidance and Phase 3.3 +4. Current task `implement.jsonl` / `check.jsonl` + +## Common Needs + +| Need | Edit location | +| --- | --- | +| Add backend/frontend/docs/test spec layer | `.trellis/spec/<layer>/` or `.trellis/spec/<package>/<layer>/` | +| Add shared thinking guides | `.trellis/spec/guides/` | +| Adjust monorepo packages | `packages` in `.trellis/config.yaml` | +| Change default package | `default_package` in `.trellis/config.yaml` | +| Control spec scanning scope | `spec_scope` in `.trellis/config.yaml` | +| Make a task read a new spec | Task `implement.jsonl` / `check.jsonl` | + +## Add A Spec Layer + +Single-repository example: + +```text +.trellis/spec/security/ +├── index.md +└── auth.md +``` + +Monorepo example: + +```text +.trellis/spec/webapp/security/ +├── index.md +└── auth.md +``` + +`index.md` should include: + +- What code this layer applies to. +- Pre-Development Checklist. +- Quality Check. +- Links to specific guideline files. + +## Update Context + +Adding a spec does not mean every task automatically reads it. The current task must reference it in JSONL: + +```bash +python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/webapp/security/index.md" "Security conventions" +python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/webapp/security/index.md" "Security review rules" +``` + +## Change Monorepo Packages + +Example `.trellis/config.yaml`: + +```yaml +packages: + webapp: + path: apps/web + api: + path: apps/api +default_package: webapp +``` + +After editing, run: + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Use this output to confirm AI can see the correct packages and spec layers. + +## Notes + +- Specs are user project conventions and can be changed according to project needs. +- Do not put temporary task information into specs; put temporary information in the task. +- Do not put long-term conventions only in agents or commands; preserve them in specs. +- After changing spec structure, check whether existing task JSONL files still point to files that exist. diff --git a/.cursor/skills/trellis-meta/references/customize-local/change-task-lifecycle.md b/.cursor/skills/trellis-meta/references/customize-local/change-task-lifecycle.md new file mode 100644 index 0000000..a7a340f --- /dev/null +++ b/.cursor/skills/trellis-meta/references/customize-local/change-task-lifecycle.md @@ -0,0 +1,90 @@ +# Change Local Task Lifecycle + +Task lifecycle includes creation, start, context configuration, finish, archive, parent/child tasks, and lifecycle hooks. The default customization targets are `.trellis/tasks/`, `.trellis/config.yaml`, and `.trellis/scripts/`. + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/config.yaml` +3. `.trellis/scripts/task.py` +4. `.trellis/scripts/common/task_store.py` +5. `.trellis/scripts/common/task_utils.py` +6. The current task's `.trellis/tasks/<task>/task.json` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Automatically sync an external system after task creation | `hooks.after_create` in `.trellis/config.yaml`. | +| Automatically update status after task start | `hooks.after_start` in `.trellis/config.yaml`. | +| Run a script after task finish | `hooks.after_finish` in `.trellis/config.yaml`. | +| Clean external resources after archive | `hooks.after_archive` in `.trellis/config.yaml`. | +| Change default task fields | `.trellis/scripts/common/task_store.py`. | +| Change task parsing/search | `.trellis/scripts/common/task_utils.py`. | +| Change active task behavior | `.trellis/scripts/common/active_task.py`. | + +## lifecycle hooks + +`.trellis/config.yaml` supports: + +```yaml +hooks: + after_create: + - "python3 .trellis/scripts/hooks/my_sync.py create" + after_start: + - "python3 .trellis/scripts/hooks/my_sync.py start" + after_finish: + - "python3 .trellis/scripts/hooks/my_sync.py finish" + after_archive: + - "python3 .trellis/scripts/hooks/my_sync.py archive" +``` + +Hook commands receive the `TASK_JSON_PATH` environment variable, pointing to the current task's `task.json`. Hook failures should usually warn, but not block the main task operation. + +## Change Task Fields + +If the user wants to add project-local fields, prefer putting them under `meta` in `task.json` to avoid breaking existing scripts' assumptions about standard fields. + +Example: + +```json +"meta": { + "linearIssue": "ENG-123", + "risk": "high" +} +``` + +If standard fields really need to change, inspect every local script that reads `task.json`. + +## Change Active Task + +Active task is session-level state stored in `.trellis/.runtime/sessions/`. Do not fall back to a global `.current-task` model. If the user wants to change active task behavior, edit: + +- `.trellis/scripts/common/active_task.py` +- platform hooks or shell session bridges +- active task descriptions in `.trellis/workflow.md` + +### `task.py create` Sets the Active Pointer + +`cmd_create` in `.trellis/scripts/common/task_store.py` calls `set_active_task` best-effort right after writing the new task directory. The behavior: + +- When the calling shell carries session identity (`TRELLIS_CONTEXT_ID` env var, or any platform-specific session env that `resolve_context_key` recognizes — see `active_task.py:_ENV_SESSION_KEYS`), the per-session pointer at `.trellis/.runtime/sessions/<context_key>.json` is rewritten to point at the new task. The task's `status=planning` and `[workflow-state:planning]` fires on the very next `UserPromptSubmit`. +- When session identity is unavailable (raw CLI invocation outside an AI session, or a platform that doesn't propagate identity to shell), the task directory is still created and `status=planning` is still written, but the active pointer is left untouched. The user can attach the task later with `task.py start <dir>` once they're back in an AI session. + +This makes `[workflow-state:planning]` the live breadcrumb during the brainstorm and JSONL curation work that follows `task.py create`. The pre-R7 behavior left the breadcrumb stuck on `no_task` until `task.py start`, so the planning block was effectively dead text. + +If you fork `task.py` to add a new creation path (e.g. an external import that bypasses `cmd_create`), audit whether your path also calls `set_active_task`. Without that call, your created tasks will not surface as active. The full status writer table is in `.trellis/spec/cli/backend/workflow-state-contract.md`. + +## Modification Steps + +1. Confirm the current task with `python3 ./.trellis/scripts/task.py current --source`. +2. Read the current task's `task.json` and confirm status and fields. +3. For configuration needs, edit `.trellis/config.yaml` first. +4. For script behavior needs, then edit `.trellis/scripts/`. +5. If the AI flow changed, synchronize `.trellis/workflow.md`. + +## Do Not + +- Do not directly edit `.trellis/.runtime/sessions/` to "fix" business state. +- Do not hard-code project-private fields into scripts; prefer `meta`. +- Do not default to asking the user to fork Trellis CLI. diff --git a/.cursor/skills/trellis-meta/references/customize-local/change-workflow.md b/.cursor/skills/trellis-meta/references/customize-local/change-workflow.md new file mode 100644 index 0000000..337c985 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/customize-local/change-workflow.md @@ -0,0 +1,65 @@ +# Change Local Workflow + +When the user wants to change Trellis phases, next-action hints, whether to create tasks, whether to use sub-agents, or when to check/wrap up, edit `.trellis/workflow.md` first. + +## Read These Files First + +1. `.trellis/workflow.md` +2. Entry files for the current platform, such as skills/commands/prompts/workflows +3. The current task's `task.json` and `prd.md` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Change phase names or phase order | `Phase Index` and the corresponding Phase sections. | +| Change whether to create a task when there is no task | `[workflow-state:no_task]` state block. | +| Change the next step during planning | Phase 1 and `[workflow-state:planning]`. | +| Change whether an agent is required during in_progress | Phase 2 and `[workflow-state:in_progress]`. | +| Change wrap-up after completion | Phase 3 and `[workflow-state:completed]`. | +| Change which skill a user intent triggers | `Skill Routing` table. | + +## Modification Steps + +1. Find the relevant section in `.trellis/workflow.md`. +2. When changing rules, keep explicit trigger conditions and next actions. +3. If adding or renaming a skill/agent, synchronize the corresponding files in platform directories. +4. Workflow-state changes only need an edit to the `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook is parser-only — it reads whatever you put in the block. Keep the opening and closing tags' STATUS strings identical (`[workflow-state:foo]…[/workflow-state:foo]`); mismatched STATUS pairs are silently dropped. +5. Make the AI reread `.trellis/workflow.md`; do not keep using rules from the old conversation. + +## Example: Relax Task Creation Requirements + +To change when task creation can be skipped, usually edit `[workflow-state:no_task]`: + +```md +[workflow-state:no_task] +Task is not required when the answer is a one-reply explanation, no files are changed, and no research is needed. +[/workflow-state:no_task] +``` + +If the formal Phase 1 flow also needs to change, synchronize the Phase 1 section. + +## Example: One Platform Does Not Use Sub-Agents + +If the user wants only one platform to avoid sub-agents, first confirm whether that platform has a separate group in the workflow. Then change Phase 2 routing for that platform group instead of deleting all `trellis-implement` / `trellis-check` instructions across platforms. + +## `/trellis:continue` Route Table + +`/trellis:continue` resumes a task by deciding which phase step to load next. The decision combines `task.json.status` with the presence of artifacts inside the task directory. The mapping is fixed in the command itself; forks that add custom statuses must extend both the workflow.md tag block and this table. + +| `status` | Artifact state | Resume at | +| --- | --- | --- | +| `planning` | `prd.md` missing | Phase 1.1 (load `trellis-brainstorm`) | +| `planning` | 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.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. + +## Notes + +`.trellis/workflow.md` is the local project workflow, not an immutable template. The user can adapt it to team habits. After editing it, platform entry files may still contain old descriptions, so inspect them too. diff --git a/.cursor/skills/trellis-meta/references/customize-local/overview.md b/.cursor/skills/trellis-meta/references/customize-local/overview.md new file mode 100644 index 0000000..b75d208 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/customize-local/overview.md @@ -0,0 +1,55 @@ +# Local Customization Overview + +This directory is for local AI working in a user project where Trellis was installed through npm and `trellis init` has already been run. The AI should modify generated `.trellis/` and platform directories inside the project, not Trellis CLI upstream source code. + +## First Determine What The User Actually Wants To Change + +| User wording | Read first | +| --- | --- | +| "Change the Trellis flow / phases / next prompt" | `change-workflow.md` | +| "Change task creation, status, archive, or hooks" | `change-task-lifecycle.md` | +| "AI did not read context / change injected content" | `change-context-loading.md` | +| "A platform hook is not behaving as expected" | `change-hooks.md` | +| "Change implement/check/research agent behavior" | `change-agents.md` | +| "Add a skill/command/workflow/prompt" | `change-skills-or-commands.md` | +| "Adjust the project spec structure" | `change-spec-structure.md` | +| "Add team conventions and local notes" | `add-project-local-conventions.md` | + +## General Operation Order + +1. **Confirm platform and directories**: inspect which directories exist, such as `.claude/`, `.codex/`, `.cursor/`, `.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. +5. **Synchronize semantics**: if a shared flow changes, check whether platform entry points also need changes; if a platform entry changes, check whether `.trellis/workflow.md` still agrees. + +## Local File Priority + +| Layer | Files | +| --- | --- | +| Workflow | `.trellis/workflow.md` | +| Project configuration | `.trellis/config.yaml` | +| Task material | `.trellis/tasks/<task>/` | +| Project specs | `.trellis/spec/` | +| Runtime scripts | `.trellis/scripts/` | +| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.zcode/`, and similar directories | +| Shared skill | `.agents/skills/` | + +## Things Not To Do By Default + +- Do not edit the global npm install directory. +- Do not edit `node_modules/@mindfoldhq/trellis`. +- Do not assume the user has the Trellis GitHub repository. +- Do not overwrite local files already modified by the user with default templates. +- Do not put team project rules into public `trellis-meta`; project rules belong in `.trellis/spec/` or a local skill. + +## When To Inspect Upstream Source + +Switch to an upstream source-code perspective only when the user explicitly expresses one of these goals: + +- "I want to open a PR to Trellis" +- "I want to change npm package publish contents" +- "I want to fork Trellis" +- "I want to modify the generation logic for `trellis init/update`" + +Otherwise, default to modifying local Trellis files inside the user project. diff --git a/.cursor/skills/trellis-meta/references/local-architecture/bundled-skills.md b/.cursor/skills/trellis-meta/references/local-architecture/bundled-skills.md new file mode 100644 index 0000000..1c819cb --- /dev/null +++ b/.cursor/skills/trellis-meta/references/local-architecture/bundled-skills.md @@ -0,0 +1,146 @@ +# 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` | +| 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`, 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/.cursor/skills/trellis-meta/references/local-architecture/context-injection.md b/.cursor/skills/trellis-meta/references/local-architecture/context-injection.md new file mode 100644 index 0000000..4a7517b --- /dev/null +++ b/.cursor/skills/trellis-meta/references/local-architecture/context-injection.md @@ -0,0 +1,68 @@ +# Local Context Injection System + +Trellis context injection aims to make AI read the right files at the right time instead of relying on model memory. In a user project, injection is implemented by `.trellis/` scripts together with platform hooks, agents, and skills. + +## Injected Context Types + +| Type | Source | Purpose | +| --- | --- | --- | +| session context | `.trellis/scripts/get_context.py` | Current developer, git status, active task, active tasks, journal, packages. | +| workflow context | `.trellis/workflow.md` | Current Trellis flow and next action. | +| spec context | `.trellis/spec/` + task JSONL | Specs that must be followed during implementation/checking. | +| task context | `.trellis/tasks/<task>/prd.md`, `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 + +Platforms with session-start support inject a Trellis overview when a session starts, clears, compacts, or receives a similar event. Injected content usually includes: + +- workflow summary. +- current task status. +- active tasks. +- spec index paths. +- developer identity and git status. + +If the user feels the AI does not know the current task in a new session, first check whether the platform's session-start hook or equivalent mechanism is installed and running. + +## workflow-state + +workflow-state is a lightweight hint injected around each user turn. Based on current task status, it selects a block from `.trellis/workflow.md`, such as `no_task`, `planning`, `in_progress`, or `completed`. + +If the user wants to change "what the AI should do next in a given state," edit the corresponding state block in `.trellis/workflow.md` first. + +## sub-agent context + +Implement and check agents need task context. Trellis has two loading modes: + +1. **hook push**: a platform hook injects 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 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 + +`implement.jsonl` and `check.jsonl` contain one JSON object per line: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend rules"} +``` + +Readers should skip seed rows without a `file` field. When configuring JSONL, the AI should include only spec/research files, not pre-register code files that will be modified. + +## Active Task And Context Key + +Active task state lives in `.trellis/.runtime/sessions/` and is isolated per session. Hooks try to resolve the context key from platform events, environment variables, transcript paths, or `TRELLIS_CONTEXT_ID`. + +If shell commands cannot see the same context key, `task.py current --source` may report no active task. In that case, check whether the platform passes session identity into the shell instead of hand-writing a global current-task file. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change session-start injected content | The platform's `session-start` hook or plugin file. | +| Change per-turn workflow-state rules | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The platform workflow-state hook parses these blocks verbatim and embeds no fallback text. | +| Change how sub-agents read context | Platform agent definitions, the `inject-subagent-context` hook, or agent preludes. | +| Change JSONL validation/display | `.trellis/scripts/common/task_context.py`. | +| Change active task resolution | `.trellis/scripts/common/active_task.py`. | + +When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct task artifacts/spec/research. diff --git a/.cursor/skills/trellis-meta/references/local-architecture/generated-files.md b/.cursor/skills/trellis-meta/references/local-architecture/generated-files.md new file mode 100644 index 0000000..5e53865 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/local-architecture/generated-files.md @@ -0,0 +1,80 @@ +# Local Files Generated After Init + +`trellis init` writes the Trellis runtime into the user project. Later, `trellis update` tries to update Trellis-managed template files, but it uses `.trellis/.template-hashes.json` to determine which files have already been modified by the user. + +This page only describes files that are visible and editable inside the user project. + +## `.trellis/` + +```text +.trellis/ +├── workflow.md +├── config.yaml +├── .developer +├── .version +├── .template-hashes.json +├── .runtime/ +├── scripts/ +├── spec/ +├── tasks/ +└── workspace/ +``` + +| Path | Usually editable? | Notes | +| --- | --- | --- | +| `.trellis/workflow.md` | Yes | Local workflow documentation and AI routing rules. | +| `.trellis/config.yaml` | Yes | Project configuration, hooks, packages, journal line limits, and related settings. | +| `.trellis/spec/` | Yes | Project specs, intended to be updated regularly by users and AI. | +| `.trellis/tasks/` | Yes | Task material and research artifacts, maintained by the task workflow. | +| `.trellis/workspace/` | Yes | Session records, usually written by `add_session.py`. | +| `.trellis/scripts/` | Carefully | Local runtime. It can be customized, but only after understanding the call chain. | +| `.trellis/.runtime/` | No | Runtime state, usually written automatically by hooks/scripts. | +| `.trellis/.developer` | Carefully | Current developer identity. | +| `.trellis/.version` | No | Trellis version record used by update/migration logic. | +| `.trellis/.template-hashes.json` | No | Template hash record. Do not hand-write business rules here. | + +## Platform Directories + +Different platforms generate different directories. Common categories: + +| Category | Example paths | Purpose | +| --- | --- | --- | +| hooks | `.claude/hooks/`, `.codex/hooks/`, `.cursor/hooks/` | Inject session context, workflow-state, and sub-agent context. | +| settings | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.trae/hooks.json` | Tell the platform when to run hooks or plugins. | +| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/`, `.zcode/cli/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/`, `.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. + +## Meaning Of Template Hashes + +`.trellis/.template-hashes.json` records the content hash from the last time Trellis wrote a template file. `trellis update` uses it to distinguish three cases: + +| Case | Update behavior | +| --- | --- | +| File was not modified by the user | It can be updated automatically. | +| File was modified by the user | Prompt the user to overwrite, keep, or generate `.new`. | +| File is no longer a current template | It may be deleted, renamed, or preserved according to migration rules. | + +When an AI customizes local Trellis files, it does not need to maintain hashes manually. It is normal for Trellis update to recognize the result as "modified by the user." + +## Local Customization Boundaries + +Editable by default: + +- `.trellis/workflow.md` +- `.trellis/config.yaml` +- `.trellis/spec/**` +- `.trellis/scripts/**` +- Platform hooks, settings, agents, skills, commands, prompts, and workflows + +Do not edit by default: + +- Global npm install directory +- `node_modules/@mindfoldhq/trellis` +- Trellis GitHub repository source code +- Concrete state files under `.trellis/.runtime/**` +- Hash contents inside `.trellis/.template-hashes.json` + +Switch to the Trellis CLI source-code perspective only when the user explicitly wants to contribute upstream. diff --git a/.cursor/skills/trellis-meta/references/local-architecture/multi-agent-channel.md b/.cursor/skills/trellis-meta/references/local-architecture/multi-agent-channel.md new file mode 100644 index 0000000..6df61eb --- /dev/null +++ b/.cursor/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/.cursor/skills/trellis-meta/references/local-architecture/overview.md b/.cursor/skills/trellis-meta/references/local-architecture/overview.md new file mode 100644 index 0000000..e97cab8 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/local-architecture/overview.md @@ -0,0 +1,51 @@ +# Local Trellis Architecture Overview + +`trellis-meta` is for user projects that have already run `trellis init`. The user's machine usually has only the npm-installed `trellis` command plus the Trellis files generated inside the project; it may not have the Trellis CLI source code. + +Therefore, when an AI uses this skill, the default customization target is local files inside the user project: + +- `.trellis/`: workflow, tasks, specs, memory, scripts, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.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. + +## Local System Model + +Trellis provides three layers inside a user project: + +1. **Workflow layer**: `.trellis/workflow.md` defines phases, routing, next actions, and prompt blocks. +2. **Persistence layer**: `.trellis/tasks/`, `.trellis/spec/`, and `.trellis/workspace/` store tasks, specs, and session memory. +3. **Platform integration layer**: hooks, settings, agents, skills, commands, prompts, and workflows in platform directories connect the Trellis workflow to different AI tools. + +All three layers live inside the user project, so an AI can read and modify them directly. + +## Core Paths + +| Path | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow phases, skill routing, and workflow-state prompt blocks. | +| `.trellis/config.yaml` | Project configuration, task lifecycle hooks, monorepo package configuration, and journal configuration. | +| `.trellis/spec/` | The user's project-specific coding conventions and thinking guides. | +| `.trellis/tasks/` | Each task's PRD, technical notes, research files, and JSONL context. | +| `.trellis/workspace/` | Per-developer journals and cross-session memory. | +| `.trellis/scripts/` | Local Python runtime used by commands, hooks, and context injection. | +| `.trellis/.runtime/` | Session-level runtime state, such as the current task pointer. | +| `.trellis/.template-hashes.json` | Template hashes for Trellis-managed files, used by update to determine whether local files were modified by the user. | + +## AI Customization Principles + +1. **Find the local source of truth first**: Do not edit from memory. Read `.trellis/workflow.md`, `.trellis/config.yaml`, the relevant platform directory, and related task files first. +2. **Edit the user project, not the npm package cache**: Modify generated files inside the project, not `node_modules` or the global npm install directory. +3. **Keep platform files aligned with `.trellis/`**: If workflow routing changes, also check whether platform skills or commands still describe the same flow. +4. **Put project-specific rules in `.trellis/spec/` or a local skill**: Do not put team conventions into `trellis-meta`. +5. **Preserve user changes**: If a file was already modified locally, work from the current content instead of overwriting it with a default template. + +## How To Use This Directory + +- To understand which files exist after init, read `generated-files.md`. +- To change phases, routing, or next actions, read `workflow.md`. +- To change the task model, JSONL context, or active task behavior, read `task-system.md`. +- To change coding convention injection, read `spec-system.md`. +- To understand journals and cross-session memory, read `workspace-memory.md`. +- To change hooks or sub-agent context loading, read `context-injection.md`. diff --git a/.cursor/skills/trellis-meta/references/local-architecture/spec-system.md b/.cursor/skills/trellis-meta/references/local-architecture/spec-system.md new file mode 100644 index 0000000..38fdf14 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/local-architecture/spec-system.md @@ -0,0 +1,102 @@ +# Local Spec System + +`.trellis/spec/` is the user's project-specific engineering spec library. Trellis is not about making AI memorize conventions; it injects relevant specs or requires the AI to read them at the right time. + +## Directory Model + +A common single-repository structure: + +```text +.trellis/spec/ +├── backend/ +│ ├── index.md +│ └── ... +├── frontend/ +│ ├── index.md +│ └── ... +└── guides/ + ├── index.md + └── ... +``` + +A common monorepo structure: + +```text +.trellis/spec/ +├── cli/ +│ ├── backend/ +│ │ ├── index.md +│ │ └── ... +│ └── unit-test/ +│ ├── index.md +│ └── ... +├── docs-site/ +│ └── docs/ +│ ├── index.md +│ └── ... +└── guides/ + ├── index.md + └── ... +``` + +`index.md` is the entry point for each layer. It should list the Pre-Development Checklist and Quality Check. Specific guidelines live in other Markdown files in the same directory. + +## Package Configuration + +`.trellis/config.yaml` can declare packages: + +```yaml +packages: + cli: + path: packages/cli + docs-site: + path: docs-site + type: submodule +default_package: cli +``` + +The AI can run: + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +This command lists packages and spec layers for the current project. Use this output as the reference when configuring context JSONL. + +## How Specs Enter Tasks + +Before a task enters implementation, 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"} +{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Test expectations"} +``` + +Sub-agents or platform preludes read these JSONL files and load the referenced specs. On platforms without sub-agent support, the AI should read the relevant specs directly according to the workflow. + +## What Specs Should Contain + +Specs should contain executable engineering conventions for the project, not generic best practices: + +- Where files should live. +- How error handling should be expressed. +- Input/output contracts for APIs, hooks, and commands. +- Patterns that are forbidden. +- Cases that require tests. +- Project-specific pitfalls and how to avoid them. + +When the AI learns a new rule during implementation or debugging, it should update `.trellis/spec/` rather than only summarizing it in chat. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Add a new spec layer | `.trellis/spec/<package>/<layer>/index.md` and corresponding guideline files. | +| Change monorepo spec mapping | `packages` / `default_package` / `spec_scope` in `.trellis/config.yaml`. | +| Change which specs AI reads before implementation | The task's `implement.jsonl`. | +| Change which specs AI reads during checking | The task's `check.jsonl`. | +| Change when specs should be updated | Phase 3.3 in `.trellis/workflow.md` and the `trellis-update-spec` skill. | + +## Boundaries + +`.trellis/spec/` is the user's project specification, not a permanent copy of Trellis built-in templates. The AI should encourage the user to update it according to the actual project code instead of treating Trellis default templates as immutable documents. diff --git a/.cursor/skills/trellis-meta/references/local-architecture/task-system.md b/.cursor/skills/trellis-meta/references/local-architecture/task-system.md new file mode 100644 index 0000000..7133495 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/local-architecture/task-system.md @@ -0,0 +1,130 @@ +# Local Task System + +The Trellis task system is stored entirely under `.trellis/tasks/` in the user project. Each task is a directory containing requirements, context, research, state, and relationship information. + +## Task Directory Structure + +```text +.trellis/tasks/ +├── 04-28-example-task/ +│ ├── task.json +│ ├── prd.md +│ ├── design.md +│ ├── implement.md +│ ├── implement.jsonl +│ ├── check.jsonl +│ └── research/ +└── archive/ + └── 2026-04/ +``` + +| File | Purpose | +| --- | --- | +| `task.json` | Task metadata: status, assignee, priority, branch, parent/child tasks, and similar fields. | +| `prd.md` | Requirements, 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. | + +## `task.json` + +`task.json` records task status and metadata. Common fields: + +| Field | Meaning | +| --- | --- | +| `id` / `name` / `title` | Task identity and title. | +| `status` | Status such as `planning`, `in_progress`, `review`, or `completed`. | +| `priority` | `P0`, `P1`, `P2`, `P3`. | +| `creator` / `assignee` | Creator and assignee. | +| `package` | Target package in a monorepo; may be empty. | +| `branch` / `base_branch` | Working branch and PR target branch. | +| `children` / `parent` | Parent/child task relationships. | +| `commit` / `pr_url` | Commit and PR information after completion. | +| `meta` | Extension fields. | + +## 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 + +The user sees a "current task," but Trellis stores active task state per session. + +```text +.trellis/.runtime/sessions/<context-key>.json +``` + +`task.py start` writes the task path into the runtime session file for the current session. `task.py current --source` shows the current task and where it came from. Different AI windows can point to different tasks without overwriting each other. + +If the platform or shell environment has no stable session identity, `task.py start` may be unable to set the active task. The AI should read the error, inspect the platform hook/session environment, and not fall back to a shared global pointer. + +## JSONL Context + +`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first. They do not replace `implement.md`; `implement.md` is the human-readable execution plan. + +Format: + +```jsonl +{"file": ".trellis/spec/cli/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-example/research/api.md", "reason": "API research"} +``` + +Rules: + +- Include spec and research files. +- Do not include code files that are about to be modified. +- Do not treat temporary conclusions in chat as the only context. +- Seed rows have no `file` field; they only prompt the AI to fill in real entries. + +## Common Commands + +```bash +python3 ./.trellis/scripts/task.py create "<title>" --slug <slug> +python3 ./.trellis/scripts/task.py start <task> +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py add-context <task> implement <file> <reason> +python3 ./.trellis/scripts/task.py validate <task> +python3 ./.trellis/scripts/task.py finish +python3 ./.trellis/scripts/task.py archive <task> +``` + +When modifying the task system, the AI should prefer script commands to maintain structure. Edit JSON/Markdown directly only when scripts do not cover the need. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change the default task template | `.trellis/scripts/common/task_store.py` and task creation instructions. | +| Change status semantics | `.trellis/workflow.md`, workflow-state hook logic, and task usage conventions. | +| Add task lifecycle actions | `hooks.after_*` in `.trellis/config.yaml`. | +| Change context rules | 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/.cursor/skills/trellis-meta/references/local-architecture/workflow.md b/.cursor/skills/trellis-meta/references/local-architecture/workflow.md new file mode 100644 index 0000000..f0659ff --- /dev/null +++ b/.cursor/skills/trellis-meta/references/local-architecture/workflow.md @@ -0,0 +1,75 @@ +# Local Workflow System + +`.trellis/workflow.md` is the Trellis workflow source of truth inside the user project. An AI does not need Trellis source code to understand how the current project should move tasks forward; this file is enough. + +## File Responsibilities + +`.trellis/workflow.md` has three responsibilities: + +1. **Explain workflow phases**: Plan, Execute, Finish. +2. **Define skill routing**: which skill or agent the AI should use when the user expresses a certain intent. +3. **Provide workflow-state prompt blocks**: hooks can inject the prompt block for the current state into the conversation. + +## Current Phase Model + +```text +Phase 1: Plan -> clarify what to build, produce prd.md and required research +Phase 2: Execute -> implement against the PRD and specs, then check +Phase 3: Finish -> final verification, preserve lessons, and wrap up +``` + +Each phase contains numbered steps, such as `1.3 Configure context`. These numbers are not runtime fields in `task.json`; they are workflow structure for AI and humans to read. + +## Skill Routing + +`workflow.md` separates routing by platform capability: + +- Platforms with sub-agent support: dispatch `trellis-implement` by default for implementation and `trellis-check` for checking. +- Platforms without sub-agent support: the main session reads skills such as `trellis-before-dev`, then executes directly. + +When changing local AI behavior, update the routing descriptions in `workflow.md` first, then check whether the corresponding platform skill, command, or agent files need to stay in sync. + +## Workflow-State Prompt Blocks + +The bottom of `workflow.md` can contain state blocks like this: + +```text +[workflow-state:no_task] +... +[/workflow-state:no_task] +``` + +Hooks choose the right block based on current task status and inject it into the conversation. Common states include: + +| State | Meaning | +| --- | --- | +| `no_task` | The current session has no active task. | +| `planning` | The task is still in requirements, research, or context configuration. | +| `in_progress` | The task has entered implementation and checking. | +| `completed` | The task is complete and waiting for wrap-up or archive. | + +If the user wants to change policies such as "whether to create a task when there is no task," "when task creation may be skipped," or "whether sub-agents are required," edit these state blocks and the routing table above them. + +## Local Modification Patterns + +Common changes: + +| Goal | Edit point | +| --- | --- | +| Add a phase | Update the Phase Index, phase body, routing, and state blocks. | +| Change task creation policy | Update the `no_task` state block and Phase 1 description. | +| Change the default implementation/check path | Update Phase 2 and skill routing. | +| Change the wrap-up flow | Update Phase 3 and `finish-work` related descriptions. Note the current split: Phase 3.4 = AI-driven code commits (batched, user-confirmed), Phase 3.5 = `/finish-work` (archive + record session). `/finish-work` refuses to run if the working tree is dirty. | +| Change platform differences | Update routing descriptions grouped by platform. | + +After editing, make the AI reread `.trellis/workflow.md`; do not assume the flow from the old conversation is still valid. + +## Relationship To Platform Files + +`workflow.md` is the semantic center of the local workflow, but each platform can also have its own entry files: + +- skills, such as `trellis-brainstorm` and `trellis-check`. +- commands/prompts/workflows, such as continue and finish-work. +- hooks, such as session-start or workflow-state injection. + +If only `workflow.md` changes, platform entry files may still contain old language. When the user wants to change "what the AI actually does," also inspect the relevant platform directory. diff --git a/.cursor/skills/trellis-meta/references/local-architecture/workspace-memory.md b/.cursor/skills/trellis-meta/references/local-architecture/workspace-memory.md new file mode 100644 index 0000000..c2958f2 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/local-architecture/workspace-memory.md @@ -0,0 +1,71 @@ +# Local Workspace Memory System + +`.trellis/workspace/` stores cross-session memory. Its purpose is to let AI and humans understand what happened before across different windows and different days. + +## Directory Structure + +```text +.trellis/workspace/ +├── index.md +└── <developer>/ + ├── index.md + ├── journal-1.md + └── journal-2.md +``` + +| File | Purpose | +| --- | --- | +| `.trellis/.developer` | Current developer identity. | +| `.trellis/workspace/index.md` | Global workspace overview. | +| `.trellis/workspace/<developer>/index.md` | Session index for a developer. | +| `.trellis/workspace/<developer>/journal-N.md` | Session journal. | + +## Developer Identity + +Run this the first time: + +```bash +python3 ./.trellis/scripts/init_developer.py <name> +``` + +This creates `.trellis/.developer` and the corresponding workspace directory. The AI should not change developer identity casually; if the identity is wrong, first confirm who is using the current project. + +## Journal + +`journal-N.md` records completed or partially completed work from each session. By default, each journal holds about 2000 lines; after that it rotates to the next file. + +Common command for recording a session: + +```bash +python3 ./.trellis/scripts/add_session.py \ + --title "Session title" \ + --summary "What changed" \ + --commit "abc1234" +``` + +Planning or review work without a commit can also be recorded by using `--no-commit` or an empty commit value. + +## Relationship Between Workspace Memory And Tasks + +| System | What it stores | +| --- | --- | +| `.trellis/tasks/` | Requirements, design, research, and state for a specific task. | +| `.trellis/workspace/` | Work records across tasks and sessions. | +| `.trellis/spec/` | Engineering knowledge preserved as long-term conventions. | + +If information is only useful for the current task, put it in the task directory. +If information describes what happened in the current session, put it in the workspace journal. +If information should be followed every time code is written in the future, put it in spec. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change maximum journal lines | `max_journal_lines` in `.trellis/config.yaml`. | +| Change session auto-commit message | `session_commit_message` in `.trellis/config.yaml`. | +| Change session content format | `.trellis/scripts/add_session.py`. | +| Change how workspace is displayed in context | `.trellis/scripts/common/session_context.py`. | + +## AI Usage Rules + +The AI should not treat workspace as the only source of truth. When resuming a task, read the current task first, then use workspace for background. After a task is complete, record important process notes in workspace; if long-term rules emerged, update spec. diff --git a/.cursor/skills/trellis-meta/references/platform-files/agents.md b/.cursor/skills/trellis-meta/references/platform-files/agents.md new file mode 100644 index 0000000..a07cae7 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/platform-files/agents.md @@ -0,0 +1,82 @@ +# Agents + +Trellis agent files define specialized roles. Common Trellis agents in a user project are: + +- `trellis-research` +- `trellis-implement` +- `trellis-check` + +File locations and formats differ by platform, but responsibility boundaries should stay consistent. + +## Agent Responsibilities + +| Agent | Responsibility | +| --- | --- | +| `trellis-research` | Investigate the question and write findings into the current task's `research/`. | +| `trellis-implement` | Implement against `prd.md`, 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. + +## Common Paths + +| Platform | Agent path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/cli/agents/trellis-*.md` | + +GitHub Copilot agent/prompt support is provided by a combination of directories such as `.github/agents/`, `.github/prompts/`, and `.github/skills/`; inspect the files actually generated in the user project. + +Main-session workflow platforms such as Kilo, Antigravity, and Devin may not have Trellis sub-agent files. They usually rely on workflows/skills to guide the main session. + +## Two Context Loading Modes + +### hook push + +The platform hook injects task context before the agent starts. The agent file itself can focus more on responsibilities and boundaries. + +Common on platforms that support agent hooks. + +### agent pull + +The agent file instructs the agent to read after startup: + +- `python3 ./.trellis/scripts/task.py current --source` +- `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. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Implement agent must follow extra restrictions | The platform's `trellis-implement` agent file. | +| Check agent must run project-specific commands | `trellis-check` agent file, and `.trellis/spec/` if needed. | +| Research agent must output a fixed format | `trellis-research` agent file. | +| Agent cannot read task context | Agent prelude or `inject-subagent-context` hook. | +| Add a project-specific agent | Platform agent directory + related workflow/command/skill entry point. | + +## Modification Principles + +1. **Keep responsibilities single-purpose**. Do not mix research, implement, and check responsibilities into one agent. +2. **Specify the read order**. Agents must know to start from the active task, 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. + +## Do Not Default To Editing Upstream Templates + +Local AI should default to modifying platform agent files inside the user project. Discuss upstream template source only when the user explicitly wants to contribute the change back to Trellis. diff --git a/.cursor/skills/trellis-meta/references/platform-files/hooks-and-settings.md b/.cursor/skills/trellis-meta/references/platform-files/hooks-and-settings.md new file mode 100644 index 0000000..d4ae8f6 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/platform-files/hooks-and-settings.md @@ -0,0 +1,72 @@ +# Hooks And Settings + +Hooks/settings are the entry layer that connects a platform to Trellis. They decide which scripts, plugins, or extensions a platform runs for which events. + +## Settings Responsibilities + +settings/config files usually register: + +- session-start hook: injects a Trellis overview when a new session starts or context resets. +- workflow-state hook: parses `[workflow-state:STATUS]` blocks from `.trellis/workflow.md` and emits the body matching the current task `status` on each user input. Parser-only; the script does not embed fallback content. +- sub-agent context hook: injects task context when implementation/check/research agents start. +- shell/session bridge: lets shell commands see the same Trellis session identity. +- platform plugin or extension entry points. + +Common files: + +| Platform | settings/config | +| --- | --- | +| Claude Code | `.claude/settings.json` | +| Cursor | `.cursor/hooks.json` | +| Codex | `.codex/hooks.json`, `.codex/config.toml` | +| OpenCode | `.opencode/package.json`, `.opencode/plugins/*` | +| Kiro | `.kiro/hooks/` + platform config | +| Gemini CLI | `.gemini/settings.json` | +| Qoder | `.qoder/settings.json` | +| CodeBuddy | `.codebuddy/settings.json` | +| GitHub Copilot | `.github/copilot/hooks.json` | +| Factory Droid | `.factory/settings.json` | +| Pi Agent | `.pi/settings.json`, `.pi/extensions/trellis/` | +| Trae IDE | `.trae/hooks.json` | + +Reasonix and ZCode are pull-based platforms that do not use hooks or settings files; their agent files contain prelude instructions to read context after startup. + +Whether these files exist in a project depends on which `trellis init --<platform>` flags the user ran. + +## Hook Script Types + +| Script | Purpose | +| --- | --- | +| `session-start.py` | Generates session-start context. | +| `inject-workflow-state.py` | Parses `[workflow-state:STATUS]` blocks in `.trellis/workflow.md` and emits the body matching the current task status. Falls back to `Refer to workflow.md for current step.` when no matching block exists. | +| `inject-subagent-context.py` | Injects PRD, JSONL context, and related spec/research into sub-agents. | +| `inject-shell-session-context.py` | Lets shell commands inherit Trellis session identity. | + +Not every platform has every hook. Do not copy files from another platform just because a platform lacks a hook; first confirm whether that platform supports the corresponding event. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| AI should see more/less context in a new session | Platform `session-start` hook. | +| Per-turn hint policy should change | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook parses workflow.md verbatim — no script edit required. | +| Sub-agent cannot read PRD/spec | `inject-subagent-context` hook or agent prelude. | +| `task.py current` in shell has no active task | Shell/session bridge hook or platform environment variable configuration. | +| Disable an automatic injection | The corresponding hook registration in settings/config. | + +## Modification Principles + +1. **Settings wire things up; hooks define behavior**. If only the hook changes, the platform may never call it. If only settings change, behavior may not change. +2. **Confirm platform event names first**. Different platforms use different names for SessionStart, UserPromptSubmit, AgentSpawn, shell execution, and similar events. +3. **Hooks read local `.trellis/`, not upstream source**. `.trellis/scripts/` and `.trellis/workflow.md` in the user project are the default targets. +4. **Errors must be visible**. Hook failures should tell the user what was not injected instead of silently leaving the AI without context. + +## Troubleshooting Path + +If the user says "AI did not read Trellis state": + +1. Check whether the platform settings register the hook. +2. Check whether the hook file exists. +3. Manually run the `.trellis/scripts/get_context.py` or `task.py current --source` command that the hook depends on. +4. Check whether active task state exists in `.trellis/.runtime/sessions/`. +5. Check whether the platform shell passes session identity. diff --git a/.cursor/skills/trellis-meta/references/platform-files/overview.md b/.cursor/skills/trellis-meta/references/platform-files/overview.md new file mode 100644 index 0000000..e403858 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/platform-files/overview.md @@ -0,0 +1,59 @@ +# Platform Files Overview + +Trellis connects the same local architecture to different AI tools. `.trellis/` stores the shared runtime; platform directories store adapter files that define how each AI tool enters Trellis. + +When a local AI modifies Trellis, it should distinguish two file categories first: + +- **Shared files**: `.trellis/workflow.md`, `.trellis/tasks/`, `.trellis/spec/`, `.trellis/scripts/`. +- **Platform files**: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.trae/`, `.kilocode/`, `.agent/`, `.devin/`, `.reasonix/`, `.zcode/`, and similar directories. + +Platform files do not store business state. They let the corresponding AI tool read Trellis state, call Trellis scripts, and load Trellis skills/agents/hooks. + +## Platform File Categories + +| Category | Common paths | Purpose | +| --- | --- | --- | +| settings/config | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.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/`, `.devin/workflows/` | Entry points explicitly invoked by the user. | + +## Three Platform Integration Modes + +### 1. Hook / Extension Driven + +These platforms can trigger scripts or plugins on specific events and actively inject Trellis context into AI. + +Common capabilities: + +- session-start injection of a `.trellis/` overview. +- workflow-state hints for each user turn. +- PRD/spec/research injection when sub-agents start. +- Shell commands inheriting session identity. + +To change "when the AI knows what," inspect hooks/plugins/extensions and settings first. + +### 2. Agent Prelude / Pull-Based + +Some platforms cannot reliably let hooks rewrite sub-agent prompts, so the agent file itself instructs the agent to read the active task, PRD, and JSONL context after startup. + +To change how sub-agents load context, inspect the agent files themselves. + +### 3. Main-Session Workflow + +Some platforms do not have Trellis sub-agent or hook capabilities. They rely on workflows/skills/commands to guide the main-session AI to read files, run scripts, and move tasks forward. + +To change behavior, inspect platform workflows/skills/commands and `.trellis/workflow.md`. + +## Local Modification Order + +When the user asks to customize behavior for a platform, the AI should inspect files in this order: + +1. Read `.trellis/workflow.md` to confirm the shared flow. +2. Read the target platform's settings/config to see which hooks/agents/skills/commands are registered. +3. Read the target platform's agents/skills/commands/hooks. +4. Modify the local file closest to the user's need. +5. If the change affects the shared flow, synchronize `.trellis/workflow.md` or `.trellis/spec/`. + +Do not modify only platform files and forget the shared workflow. Do not modify only `.trellis/workflow.md` and forget that platform entry points may still contain old descriptions. diff --git a/.cursor/skills/trellis-meta/references/platform-files/platform-map.md b/.cursor/skills/trellis-meta/references/platform-files/platform-map.md new file mode 100644 index 0000000..dddb743 --- /dev/null +++ b/.cursor/skills/trellis-meta/references/platform-files/platform-map.md @@ -0,0 +1,88 @@ +# Platform File Map + +This page lists common Trellis file locations in a user project by platform. Whether a platform directory exists in an actual project depends on which `trellis init --<platform>` commands the user ran. + +## Matrix + +| Platform | CLI flag | Main directory | Skill directory | Agent directory | Hooks/extensions | +| --- | --- | --- | --- | --- | --- | +| Claude Code | `--claude` | `.claude/` | `.claude/skills/` | `.claude/agents/` | `.claude/hooks/` + `.claude/settings.json` | +| Cursor | `--cursor` | `.cursor/` | `.cursor/skills/` | `.cursor/agents/` | `.cursor/hooks.json` + `.cursor/hooks/` | +| OpenCode | `--opencode` | `.opencode/` | `.opencode/skills/` | `.opencode/agents/` | `.opencode/plugins/` | +| Codex | `--codex` | `.codex/` | `.agents/skills/` | `.codex/agents/` | `.codex/hooks/` + `.codex/hooks.json` | +| Kilo | `--kilo` | `.kilocode/` | `.kilocode/skills/` | Usually none | `.kilocode/workflows/` | +| Kiro | `--kiro` | `.kiro/` | `.kiro/skills/` | `.kiro/agents/` | `.kiro/hooks/` | +| Gemini CLI | `--gemini` | `.gemini/` | `.agents/skills/` | `.gemini/agents/` | `.gemini/settings.json` + `.gemini/hooks/` | +| Antigravity | `--antigravity` | `.agent/` | `.agent/skills/` | Usually none | `.agent/workflows/` | +| 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/` (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/` | `.agents/skills/` | `.zcode/cli/agents/` | pull-based prelude (no hooks) | + +## Capability Groups + +### Trellis Sub-Agent Support + +These platforms usually have `trellis-research`, `trellis-implement`, and `trellis-check` files: + +- Claude Code +- Cursor +- OpenCode +- Codex +- Kiro +- Gemini CLI +- Qoder +- CodeBuddy +- GitHub Copilot +- Factory Droid +- Pi Agent +- Trae IDE +- Reasonix (delivered as skills with `runAs: subagent` under `.reasonix/skills/`, not as a separate `agents/` directory) +- ZCode + +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 +- 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. + +## Decision Rules When Modifying Platform Files + +1. User specified a platform: modify only that platform directory unless shared workflow/spec files must also change. +2. User says "all platforms should do this": synchronize equivalent entry points platform by platform; do not modify only one directory. +3. User only says "my AI": inspect the configuration directories that actually exist in the project and infer the current AI platform. +4. User wants project rules: prefer `.trellis/spec/` or a project-local skill. +5. User wants Trellis behavior: edit `.trellis/workflow.md` plus platform hooks/agents/skills/commands. + +## When Paths Differ + +Platform ecosystems change, and user projects may already be customized. If this table disagrees with local files, use the actual settings/config in the user project as authoritative: + +- Check the hook that settings registers. +- Check the script that a command/prompt/workflow points to. +- Judge behavior by the read rules currently written in the agent file. + +Do not delete a custom file just because it is not listed in this path table. diff --git a/.cursor/skills/trellis-meta/references/platform-files/skills-and-commands.md b/.cursor/skills/trellis-meta/references/platform-files/skills-and-commands.md new file mode 100644 index 0000000..4732c6a --- /dev/null +++ b/.cursor/skills/trellis-meta/references/platform-files/skills-and-commands.md @@ -0,0 +1,85 @@ +# Skills, Commands, Prompts, And Workflows + +Skills and commands are textual entry points for user interaction with Trellis. Different platforms use different names, but their core purpose is the same: tell the AI how to enter the Trellis flow when the user expresses a certain intent. + +## Conceptual Differences + +| Type | Trigger mode | Best for | +| --- | --- | --- | +| skill | AI auto-match or explicit user mention | Long-term capabilities, workflow rules, modification guides. | +| command | Explicit user invocation | Clear operation entry points such as continue and finish-work. | +| prompt | Explicit user invocation or platform selection | Similar to command, but in a platform prompt format. | +| workflow | Explicit user selection or platform auto-match | Guides the main session when no sub-agent/hook exists. | + +Trellis workflow skills usually share one semantic set: brainstorm, before-dev, check, update-spec, break-loop. Multi-file built-in skills such as `trellis-meta` use layered references. + +## Common Paths + +| Platform | Common entries | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| Kilo | `.kilocode/skills/`, `.kilocode/workflows/` | +| Kiro | `.kiro/skills/` | +| Gemini CLI | `.agents/skills/`, `.gemini/commands/` | +| Antigravity | `.agent/skills/`, `.agent/workflows/` | +| 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/` | +| Reasonix | `.reasonix/skills/` | +| ZCode | `.agents/skills/`, `.zcode/commands/` | + +In a user project, use the files actually generated by init as authoritative. + +## Skill Structure + +A common skill is a directory: + +```text +trellis-meta/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should tell the AI: + +- When to use this skill. +- Which reference to read first for the current task. +- What not to do. + +References hold longer explanations so the entry file does not contain everything. + +## Command/Prompt/Workflow Structure + +Commands, prompts, and workflows are usually single files. Their content should include: + +- When to use it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +They should not store task state; task state belongs in `.trellis/tasks/` and `.trellis/.runtime/`. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Change AI auto-trigger rules | The corresponding skill's frontmatter description. | +| Change user command behavior | The corresponding command/prompt/workflow file. | +| Add a project-local skill | Platform skill directory, or shared `.agents/skills/`. | +| Let multiple platforms share one capability | Write equivalent skills in each platform skill directory, or use the `.agents/skills/` shared layer on platforms that support it. | +| Change finish/continue entry points | Platform commands/prompts/workflows. | + +## Modification Principles + +1. **Keep entry files short; references carry long content**. This matters especially for multi-file skills like `trellis-meta`. +2. **Make trigger descriptions specific**. A description that is too broad can mis-trigger; one that is too narrow may not trigger. +3. **Keep the same semantics consistent across platforms**. File formats can differ, but behavior descriptions should match. +4. **Put project-specific capabilities in local skills**. Do not put team-private flows into public `trellis-meta`. + +If the user only wants local AI to know one more project rule, usually create a project-local skill or update `.trellis/spec/` instead of changing a Trellis built-in workflow skill. diff --git a/.cursor/skills/trellis-session-insight/SKILL.md b/.cursor/skills/trellis-session-insight/SKILL.md new file mode 100644 index 0000000..8e2cbb7 --- /dev/null +++ b/.cursor/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, and Pi Agent conversation logs (the JSONL files each platform stores under `~/.claude/projects/`, `~/.codex/sessions/`, and `~/.pi/agent/sessions/`) and lets you list, search, slice by Trellis task boundaries, and dump cleaned dialogue from them. 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/.cursor/skills/trellis-session-insight/references/cli-quick-reference.md b/.cursor/skills/trellis-session-insight/references/cli-quick-reference.md new file mode 100644 index 0000000..78540f2 --- /dev/null +++ b/.cursor/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/.cursor/skills/trellis-session-insight/references/triggering-patterns.md b/.cursor/skills/trellis-session-insight/references/triggering-patterns.md new file mode 100644 index 0000000..66021ca --- /dev/null +++ b/.cursor/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/.cursor/skills/trellis-spec-bootstrap/SKILL.md b/.cursor/skills/trellis-spec-bootstrap/SKILL.md new file mode 100644 index 0000000..e1650df --- /dev/null +++ b/.cursor/skills/trellis-spec-bootstrap/SKILL.md @@ -0,0 +1,41 @@ +--- +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 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. + +## Workflow + +1. Confirm Trellis is initialized and inspect the current `.trellis/spec/` tree. +2. Analyze the repository architecture with the best available tools: GitNexus, ABCoder, language tooling, and direct source reads. +3. Decompose the spec work by package and layer only when that reflects the actual codebase. +4. Fill or reshape the spec files with concrete patterns, file paths, examples, and anti-patterns from the project. +5. Verify that the final specs are internally consistent and contain no template placeholders. + +## Reference Routing + +| Need | Read | +|------|------| +| Repository architecture analysis | [references/repository-analysis.md](references/repository-analysis.md) | +| Spec work decomposition and task planning | [references/spec-task-planning.md](references/spec-task-planning.md) | +| Writing high-signal Trellis spec files | [references/spec-writing.md](references/spec-writing.md) | +| GitNexus and ABCoder MCP setup | [references/mcp-setup.md](references/mcp-setup.md) | + +## Operating Rules + +- Treat templates as starting points, not contracts. Delete, rename, split, or add spec files when the repository calls for it. +- Prefer source-backed rules over generic advice. Every important recommendation should point at a real file or repeated local pattern. +- Keep execution single-owner by default. Optional helper agents are an implementation detail, not a requirement or user-visible dependency. +- Do not write platform-specific instructions unless the target project already standardizes on that platform. +- Do not leave placeholder text, empty headings, or copied boilerplate in `.trellis/spec/`. + +## Done Criteria + +- `.trellis/spec/` describes the project as it exists now. +- Each relevant package or layer has practical coding guidance with real examples. +- Non-applicable template sections are removed. +- `index.md` files match the final spec file set. +- Any required setup or analysis assumptions are documented in the relevant spec or task notes. diff --git a/.cursor/skills/trellis-spec-bootstrap/references/mcp-setup.md b/.cursor/skills/trellis-spec-bootstrap/references/mcp-setup.md new file mode 100644 index 0000000..629fcbd --- /dev/null +++ b/.cursor/skills/trellis-spec-bootstrap/references/mcp-setup.md @@ -0,0 +1,90 @@ +# MCP Setup + +GitNexus and ABCoder are recommended when bootstrapping Trellis specs because they expose architecture and AST context to the agent. They are tool choices, not platform requirements. Configure them through whatever MCP mechanism your agent host provides. + +## GitNexus + +GitNexus builds a code knowledge graph from the repository. Use it for module boundaries, execution flows, dependency relationships, blast radius, and graph queries. + +### Install and Index + +```bash +# Run from the repository root. +npx gitnexus analyze + +# Check index status. +npx gitnexus status + +# Re-index after code changes when the analysis is stale. +npx gitnexus analyze +``` + +The index is written to `.gitnexus/`. Keep embeddings only if the project already uses them; otherwise a normal index is enough for spec bootstrapping. + +### MCP Server Command + +Use this server command in the host's MCP configuration: + +```bash +npx -y gitnexus mcp +``` + +### Useful Tools + +| Tool | Purpose | +|------|---------| +| `gitnexus_query` | Find execution flows and functional areas by concept | +| `gitnexus_context` | Inspect callers, callees, references, and process participation for a symbol | +| `gitnexus_impact` | Understand blast radius before changing a symbol | +| `gitnexus_detect_changes` | Check changed symbols and affected flows before finishing | +| `gitnexus_cypher` | Run direct graph queries | +| `gitnexus_list_repos` | List indexed repositories | + +## ABCoder + +ABCoder parses code into UniAST and gives precise package, file, and node-level structure. Use it for signatures, type shapes, implementations, dependencies, and reverse references. + +### Install + +```bash +go install github.com/cloudwego/abcoder@latest +abcoder --help +``` + +### Parse Repositories + +```bash +abcoder parse /absolute/path/to/package \ + --lang typescript \ + --name package-name \ + --output ~/abcoder-asts +``` + +For monorepos, parse each package with a stable `--name` so task notes can reference the same repository names. + +### MCP Server Command + +Use this server command in the host's MCP configuration: + +```bash +abcoder mcp ~/abcoder-asts +``` + +### Useful Tools + +| Tool | Layer | Purpose | +|------|-------|---------| +| `list_repos` | 1 | List parsed repositories | +| `get_repo_structure` | 2 | Inspect packages and files | +| `get_package_structure` | 3 | Inspect nodes within a package | +| `get_file_structure` | 3 | Inspect functions, classes, types, and signatures in a file | +| `get_ast_node` | 4 | Retrieve code, dependencies, references, and implementations | + +## Verification + +After configuration, verify from the agent host that both MCP servers are visible. Then run one simple query against each server before starting the spec writing pass. + +```bash +ls .gitnexus/meta.json +ls ~/abcoder-asts/*.json +``` diff --git a/.cursor/skills/trellis-spec-bootstrap/references/repository-analysis.md b/.cursor/skills/trellis-spec-bootstrap/references/repository-analysis.md new file mode 100644 index 0000000..1309d29 --- /dev/null +++ b/.cursor/skills/trellis-spec-bootstrap/references/repository-analysis.md @@ -0,0 +1,59 @@ +# Repository Analysis + +The goal is to discover the project's real architecture before writing rules. Do not start from generic spec templates and fill blanks. Start from the code, then let the spec structure follow. + +## Analysis Order + +1. Read the existing `.trellis/spec/` tree and note which files are templates, outdated, or already project-specific. +2. Inspect package manifests, build scripts, workspace config, and top-level documentation to identify packages and runtime layers. +3. Use GitNexus for execution flows, module clusters, dependency hubs, and impact-sensitive areas. +4. Use ABCoder or language-native tooling for exact signatures, types, class boundaries, and implementation examples. +5. Read representative source and test files directly before turning any finding into a spec rule. + +## What To Capture + +| Area | Questions | +|------|-----------| +| Package boundaries | What does each package own? What imports cross boundaries? | +| Runtime layers | Which code is CLI, backend, frontend, worker, shared library, test-only, or tooling? | +| Core abstractions | Which types, services, stores, commands, routes, or adapters define the system shape? | +| Data flow | Where does user input enter, how is it validated, and where does state persist? | +| Error handling | How are failures represented, logged, surfaced, and tested? | +| Configuration | Where do defaults, environment config, generated files, and templates live? | +| Tests | Which test styles are trusted examples for new work? | + +## GitNexus Usage + +Start broad, then inspect specific symbols: + +```text +gitnexus_query({query: "CLI command execution flow"}) +gitnexus_query({query: "template generation and migration"}) +gitnexus_context({name: "SymbolName"}) +gitnexus_cypher({query: "MATCH (n)-[r]->(m) RETURN n.name, type(r), m.name LIMIT 30"}) +``` + +Use GitNexus results to find important files and flows. Do not quote graph output as the final authority until you have checked the relevant source files. + +## ABCoder Usage + +Use ABCoder when the spec needs exact code shapes: + +```text +list_repos() +get_repo_structure({repo_name: "package-name"}) +get_file_structure({repo_name: "package-name", file_path: "src/example.ts"}) +get_ast_node({repo_name: "package-name", node_ids: [{mod_path: "...", pkg_path: "...", name: "SymbolName"}]}) +``` + +ABCoder is most valuable for documenting constructor patterns, function signatures, type contracts, and reference chains. + +## Analysis Notes + +Keep short notes while analyzing. The notes should include: + +- Package or layer name. +- Files that define the local pattern. +- Rules the spec should teach. +- Anti-patterns found in old code, comments, tests, or migration paths. +- Spec files that should be created, deleted, renamed, or merged. diff --git a/.cursor/skills/trellis-spec-bootstrap/references/spec-task-planning.md b/.cursor/skills/trellis-spec-bootstrap/references/spec-task-planning.md new file mode 100644 index 0000000..dca2687 --- /dev/null +++ b/.cursor/skills/trellis-spec-bootstrap/references/spec-task-planning.md @@ -0,0 +1,61 @@ +# Spec Task Planning + +Use a single agent as the default execution model. The agent may create Trellis tasks for traceability, but the skill should not require a specific platform, CLI, or parallel worker model. + +## Decomposition + +Create spec work units around real ownership boundaries: + +- One package when a package has its own conventions. +- One layer when the same package has distinct frontend, backend, CLI, worker, or shared-library rules. +- One cross-cutting guide when a pattern spans packages and is not owned by one layer. + +Avoid artificial decomposition. A small library usually needs one focused spec pass, not several tasks. + +## Task Shape + +When a Trellis task is useful, write a concise PRD with these sections: + +```markdown +# Fill <package-or-layer> Trellis Specs + +## Goal +Write project-specific `.trellis/spec/` guidance for <scope>. + +## Scope +- Spec directory: +- Source directories to inspect: +- Tests to inspect: +- Out of scope: + +## Architecture Context +Summarize the concrete findings from repository analysis. + +## Files To Create Or Update +- `.trellis/spec/.../index.md` +- `.trellis/spec/.../<topic>.md` + +## Rules +- Adapt the spec file set to the real codebase. +- Use real source examples with file paths. +- Remove template-only sections that do not apply. +- Do not modify product source code unless the task explicitly asks for it. + +## Acceptance Criteria +- [ ] Specs contain concrete examples and anti-patterns from the repository. +- [ ] No placeholder text remains. +- [ ] Index files match the final spec files. +- [ ] Claims are backed by source files, tests, or project docs. +``` + +## Optional Helper Agents + +If the host supports subagents, helpers can inspect independent packages or run verification. They are optional. The main agent still owns integration and final quality. + +Helper tasks must have clear ownership: + +- Read-only research tasks may inspect any source needed for the assigned scope. +- Write tasks should own disjoint spec directories. +- Verification tasks should check placeholder removal, broken links, and consistency. + +Do not encode helper-agent names, vendor-specific commands, or platform-specific routing in the skill. Put only the required work and acceptance criteria in the task. diff --git a/.cursor/skills/trellis-spec-bootstrap/references/spec-writing.md b/.cursor/skills/trellis-spec-bootstrap/references/spec-writing.md new file mode 100644 index 0000000..6bc7dec --- /dev/null +++ b/.cursor/skills/trellis-spec-bootstrap/references/spec-writing.md @@ -0,0 +1,70 @@ +# Spec Writing + +Trellis specs are coding guidance for future agents. They should explain how to work in this repository, not how a generic project might be organized. + +## Write From Evidence + +Each important rule should be backed by one of these: + +- A source file that demonstrates the preferred pattern. +- A test file that shows expected behavior. +- A project document that defines the convention. +- A repeated pattern across multiple files. + +Use short snippets only when they make the rule clearer. Prefer linking to the file path and naming the symbol or behavior. + +## File Structure + +Keep the spec tree aligned with the project: + +- Keep `index.md` as the navigation file for the spec directory. +- Split topics when developers would look for them independently. +- Merge topics when separate files would repeat the same rule. +- Delete template files that do not apply. +- Add new files for important local patterns the template missed. + +## Content Standards + +Good spec sections include: + +- When the rule applies. +- The local pattern to follow. +- The source or test files that prove the pattern. +- Common mistakes or anti-patterns. +- Verification commands or checks when they are specific and reliable. + +Avoid: + +- Placeholder prose. +- Generic framework advice. +- Tool instructions that only work in one agent host. +- Long copied code blocks. +- Rules based on a single accidental implementation detail. + +## Example Shape + +```markdown +## Command Handlers + +Command handlers should keep argument parsing, validation, and side effects separate. The local pattern is: + +- Parse CLI flags at the command boundary. +- Convert raw inputs into typed task options before invoking core logic. +- Keep filesystem writes in the command or service layer, not in template helpers. + +Reference files: +- `packages/cli/src/commands/example.ts` +- `packages/cli/test/commands/example.test.ts` + +Avoid passing raw `process.argv` or unvalidated config objects into shared helpers. +``` + +## Final Pass + +Before finishing: + +```bash +grep -R "To be filled\\|TODO: fill\\|placeholder" .trellis/spec +``` + +Also check links, index files, and whether any spec still describes a template rather than this repository. diff --git a/.cursor/skills/trellis-update-spec/SKILL.md b/.cursor/skills/trellis-update-spec/SKILL.md new file mode 100644 index 0000000..2b828a5 --- /dev/null +++ b/.cursor/skills/trellis-update-spec/SKILL.md @@ -0,0 +1,356 @@ +--- +name: trellis-update-spec +description: "Captures executable contracts and coding conventions into .trellis/spec/ documents. Use when learning something valuable from debugging, implementing, or discussion that should be preserved for future sessions." +--- + +# Update Code-Spec - Capture Executable Contracts + +When you learn something valuable (from debugging, implementing, or discussion), use this to update the relevant code-spec documents. + +**Timing**: After completing a task, fixing a bug, or discovering a new pattern + +--- + +## Code-Spec First Rule (CRITICAL) + +In this project, "spec" for implementation work means **code-spec**: +- Executable contracts (not principle-only text) +- Concrete signatures, payload fields, env keys, and boundary behavior +- Testable validation/error behavior + +If the change touches infra or cross-layer contracts, code-spec depth is mandatory. + +### Mandatory Triggers + +Apply code-spec depth when the change includes any of: +- New/changed command or API signature +- Cross-layer request/response contract change +- Database schema/migration change +- Infra integration (storage, queue, cache, secrets, env wiring) + +### Mandatory Output (7 Sections) + +For triggered tasks, include all sections below: +1. Scope / Trigger +2. Signatures (command/API/DB) +3. Contracts (request/response/env) +4. Validation & Error Matrix +5. Good/Base/Bad Cases +6. Tests Required (with assertion points) +7. Wrong vs Correct (at least one pair) + +--- + +## When to Update Code-Specs + +| Trigger | Example | Target Spec | +|---------|---------|-------------| +| **Implemented a feature** | Added a new integration or module | Relevant spec file | +| **Made a design decision** | Chose extensibility pattern over simplicity | Relevant spec + "Design Decisions" section | +| **Fixed a bug** | Found a subtle issue with error handling | Relevant spec (e.g., error-handling docs) | +| **Discovered a pattern** | Found a better way to structure code | Relevant spec file | +| **Hit a gotcha** | Learned that X must be done before Y | Relevant spec + "Common Mistakes" section | +| **Established a convention** | Team agreed on naming pattern | Quality guidelines | +| **New thinking trigger** | "Don't forget to check X before doing Y" | `guides/*.md` (as a checklist item) | + +**Key Insight**: Code-spec updates are NOT just for problems. Every feature implementation contains design decisions and contracts that future AI/developers need to execute safely. + +--- + +## Spec Structure Overview + +``` +.trellis/spec/ +├── <layer>/ # Per-layer coding standards (e.g., backend/, frontend/, api/) +│ ├── index.md # Overview and links +│ └── *.md # Topic-specific guidelines +└── guides/ # Thinking checklists (NOT coding specs!) + ├── index.md # Guide index + └── *.md # Topic-specific guides +``` + +### CRITICAL: Code-Spec vs Guide - Know the Difference + +| Type | Location | Purpose | Content Style | +|------|----------|---------|---------------| +| **Code-Spec** | `<layer>/*.md` | Tell AI "how to implement safely" | Signatures, contracts, matrices, cases, test points | +| **Guide** | `guides/*.md` | Help AI "what to think about" | Checklists, questions, pointers to specs | + +**Decision Rule**: Ask yourself: + +- "This is **how to write** the code" → Put in a spec layer directory +- "This is **what to consider** before writing" → Put in `guides/` + +**Example**: + +| Learning | Wrong Location | Correct Location | +|----------|----------------|------------------| +| "Use API X not API Y for this task" | ❌ `guides/` (too specific for a thinking guide) | ✅ Relevant spec file (concrete convention) | +| "Remember to check X when doing Y" | ❌ Spec file (too abstract for a spec) | ✅ `guides/` (thinking checklist) | + +**Guides should be short checklists that point to specs**, not duplicate the detailed rules. + +--- + +## Update Process + +### Step 1: Identify What You Learned + +Answer these questions: + +1. **What did you learn?** (Be specific) +2. **Why is it important?** (What problem does it prevent?) +3. **Where does it belong?** (Which spec file?) + +### Step 2: Classify the Update Type + +| Type | Description | Action | +|------|-------------|--------| +| **Design Decision** | Why we chose approach X over Y | Add to "Design Decisions" section | +| **Project Convention** | How we do X in this project | Add to relevant section with examples | +| **New Pattern** | A reusable approach discovered | Add to "Patterns" section | +| **Forbidden Pattern** | Something that causes problems | Add to "Anti-patterns" or "Don't" section | +| **Common Mistake** | Easy-to-make error | Add to "Common Mistakes" section | +| **Convention** | Agreed-upon standard | Add to relevant section | +| **Gotcha** | Non-obvious behavior | Add warning callout | + +### Step 3: Read the Target Code-Spec + +Before editing, read the current code-spec to: +- Understand existing structure +- Avoid duplicating content +- Find the right section for your update + +```bash +cat .trellis/spec/<category>/<file>.md +``` + +### Step 4: Make the Update + +Follow these principles: + +1. **Be Specific**: Include concrete examples, not just abstract rules +2. **Explain Why**: State the problem this prevents +3. **Show Contracts**: Add signatures, payload fields, and error behavior +4. **Show Code**: Add code snippets for key patterns +5. **Keep it Short**: One concept per section + +### Step 5: Update the Index (if needed) + +If you added a new section or the code-spec status changed, update the category's `index.md`. + +--- + +## Update Templates + +### Mandatory Template for Infra/Cross-Layer Work + +```markdown +## Scenario: <name> + +### 1. Scope / Trigger +- Trigger: <why this requires code-spec depth> + +### 2. Signatures +- Backend command/API/DB signature(s) + +### 3. Contracts +- Request fields (name, type, constraints) +- Response fields (name, type, constraints) +- Environment keys (required/optional) + +### 4. Validation & Error Matrix +- <condition> -> <error> + +### 5. Good/Base/Bad Cases +- Good: ... +- Base: ... +- Bad: ... + +### 6. Tests Required +- Unit/Integration/E2E with assertion points + +### 7. Wrong vs Correct +#### Wrong +... +#### Correct +... +``` + +### Adding a Design Decision + +```markdown +### Design Decision: [Decision Name] + +**Context**: What problem were we solving? + +**Options Considered**: +1. Option A - brief description +2. Option B - brief description + +**Decision**: We chose Option X because... + +**Example**: +\`\`\`typescript +// How it's implemented +code example +\`\`\` + +**Extensibility**: How to extend this in the future... +``` + +### Adding a Project Convention + +```markdown +### Convention: [Convention Name] + +**What**: Brief description of the convention. + +**Why**: Why we do it this way in this project. + +**Example**: +\`\`\`typescript +// How to follow this convention +code example +\`\`\` + +**Related**: Links to related conventions or specs. +``` + +### Adding a New Pattern + +```markdown +### Pattern Name + +**Problem**: What problem does this solve? + +**Solution**: Brief description of the approach. + +**Example**: +\`\`\` +// Good +code example + +// Bad +code example +\`\`\` + +**Why**: Explanation of why this works better. +``` + +### Adding a Forbidden Pattern + +```markdown +### Don't: Pattern Name + +**Problem**: +\`\`\` +// Don't do this +bad code example +\`\`\` + +**Why it's bad**: Explanation of the issue. + +**Instead**: +\`\`\` +// Do this instead +good code example +\`\`\` +``` + +### Adding a Common Mistake + +```markdown +### Common Mistake: Description + +**Symptom**: What goes wrong + +**Cause**: Why this happens + +**Fix**: How to correct it + +**Prevention**: How to avoid it in the future +``` + +### Adding a Gotcha + +```markdown +> **Warning**: Brief description of the non-obvious behavior. +> +> Details about when this happens and how to handle it. +``` + +--- + +## Interactive Mode + +If you're unsure what to update, answer these prompts: + +1. **What did you just finish?** + - [ ] Fixed a bug + - [ ] Implemented a feature + - [ ] Refactored code + - [ ] Had a discussion about approach + +2. **What did you learn or decide?** + - Design decision (why X over Y) + - Project convention (how we do X) + - Non-obvious behavior (gotcha) + - Better approach (pattern) + +3. **Would future AI/developers need to know this?** + - To understand how the code works → Yes, update spec + - To maintain or extend the feature → Yes, update spec + - To avoid repeating mistakes → Yes, update spec + - Purely one-off implementation detail → Maybe skip + +4. **Which area does it relate to?** + - [ ] Backend code + - [ ] Frontend code + - [ ] Cross-layer data flow + - [ ] Code organization/reuse + - [ ] Quality/testing + +--- + +## Quality Checklist + +Before finishing your code-spec update: + +- [ ] Is the content specific and actionable? +- [ ] Did you include a code example? +- [ ] Did you explain WHY, not just WHAT? +- [ ] Did you include executable signatures/contracts? +- [ ] Did you include validation and error matrix? +- [ ] Did you include Good/Base/Bad cases? +- [ ] Did you include required tests with assertion points? +- [ ] Is it in the right code-spec file? +- [ ] Does it duplicate existing content? +- [ ] Would a new team member understand it? + +--- + +## Relationship to Other Commands + +``` +Development Flow: + Learn something → /trellis-update-spec → Knowledge captured + ↑ ↓ + /trellis-break-loop ←──────────────────── Future sessions benefit + (deep bug analysis) +``` + +- `/trellis-break-loop` - Analyzes bugs deeply, often reveals spec updates needed +- `/trellis-update-spec` - Actually makes the updates +- `/trellis-finish-work` - Reminds you to check if specs need updates + +--- + +## Core Philosophy + +> **Code-specs are living documents. Every debugging session, every "aha moment" is an opportunity to make the implementation contract clearer.** + +The goal is **institutional memory**: +- What one person learns, everyone benefits from +- What AI learns in one session, persists to future sessions +- Mistakes become documented guardrails diff --git a/.pi/agents/trellis-check.md b/.pi/agents/trellis-check.md new file mode 100644 index 0000000..706b5e2 --- /dev/null +++ b/.pi/agents/trellis-check.md @@ -0,0 +1,63 @@ +--- +name: trellis-check +description: | + Code quality check expert. Reviews changes against Trellis specs, fixes issues directly, and verifies quality gates. +tools: read, write, edit, bash, find, grep +--- + +## 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 `<task-path>/check.jsonl` — JSONL list of spec/research files relevant to this agent. +2. For each entry in the JSONL, Read its `file` path — these are the specs and research notes you must follow. + **Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran). +3. Read the task's `prd.md` (requirements), then `design.md` if present (technical design), then `implement.md` if present (execution plan). + +If `check.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read the task artifacts, 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 — lightweight tasks may be PRD-only, while complex tasks may also include `design.md` and `implement.md`. + +If the resolved task path has no `prd.md`, ask the user what to work on; do NOT proceed without context. + +--- + +# Check Agent + +You are the Check Agent in the Trellis workflow. + +## Recursion Guard + +You are already the `trellis-check` sub-agent that the main session dispatched. Do the review and fixes directly. + +- Do NOT spawn another `trellis-check` or `trellis-implement` sub-agent. +- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. +- Only the main session may dispatch Trellis implement/check agents. If more implementation work is needed, report that recommendation instead of spawning. + +## Core Responsibilities + +1. Inspect the current git diff. +2. Read `prd.md`, `design.md` if present, and `implement.md` if present. +3. Read and follow the spec and research files listed in the task's `check.jsonl`. +4. Review all changed code against the task artifacts and project specs. +5. Fix issues directly when they are within scope. +6. Run the relevant lint, typecheck, and focused tests available for the touched code. + +## Review Priorities + +- Behavioral regressions and missing requirements. +- Spec or platform contract violations. +- Missing or weak tests for logic changes. +- Cross-platform path, command, and encoding assumptions. + +## Output + +Report findings fixed, files changed, and verification results. If no issues remain, say that clearly. diff --git a/.pi/agents/trellis-implement.md b/.pi/agents/trellis-implement.md new file mode 100644 index 0000000..4d5d4c0 --- /dev/null +++ b/.pi/agents/trellis-implement.md @@ -0,0 +1,68 @@ +--- +name: trellis-implement +description: | + Code implementation expert. Understands Trellis specs and requirements, then implements features. No git commit allowed. +tools: read, write, edit, bash, find, grep +--- + +## 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 `<task-path>/implement.jsonl` — JSONL list of spec/research files relevant to this agent. +2. For each entry in the JSONL, Read its `file` path — these are the specs and research notes you must follow. + **Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran). +3. Read the task's `prd.md` (requirements), then `design.md` if present (technical design), then `implement.md` if present (execution plan). + +If `implement.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read the task artifacts, 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 — lightweight tasks may be PRD-only, while complex tasks may also include `design.md` and `implement.md`. + +If the resolved task path has no `prd.md`, ask the user what to work on; do NOT proceed without context. + +--- + +# Implement Agent + +You are the Implement Agent in the Trellis workflow. + +## Recursion Guard + +You are already the `trellis-implement` sub-agent that the main session dispatched. Do the implementation work directly. + +- Do NOT spawn another `trellis-implement` or `trellis-check` sub-agent. +- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. +- Only the main session may dispatch Trellis implement/check agents. If more parallel work is needed, report that recommendation instead of spawning. + +## Core Responsibilities + +1. Understand the active task requirements. +2. Read `prd.md`, `design.md` if present, and `implement.md` if present. +3. Read and follow the spec and research files listed in the task's `implement.jsonl`. +4. Implement the requested change using existing project patterns. +5. Run the relevant lint, typecheck, and focused tests available for the touched code. +6. Report files changed and verification results. + +## Forbidden Operations + +Do not run: + +- `git commit` +- `git push` +- `git merge` + +## Working Rules + +- Read adjacent code and tests before editing. +- Keep changes scoped to the task. +- Do not revert unrelated user or concurrent changes. +- Fix root causes rather than masking symptoms. +- Prefer existing local helpers and platform patterns over new abstractions. diff --git a/.pi/agents/trellis-research.md b/.pi/agents/trellis-research.md new file mode 100644 index 0000000..d29d859 --- /dev/null +++ b/.pi/agents/trellis-research.md @@ -0,0 +1,25 @@ +--- +name: trellis-research +description: | + Code and technical research expert. Finds relevant files, patterns, docs, and persists findings to the current task's research/ directory. +tools: read, write, bash, find, grep +--- +# Research Agent + +You are the Research Agent in the Trellis workflow. + +## Core Principle + +Persist every finding to a file. Chat context is temporary; files under the task directory survive compaction and handoff. + +## Core Responsibilities + +1. Resolve the active task with `python3 ./.trellis/scripts/task.py current --source`. +2. Create `<task-dir>/research/` when it does not exist. +3. Search internal code, specs, and relevant external documentation. +4. Write each distinct topic to `<task-dir>/research/<topic-slug>.md`. +5. Report only file paths and concise summaries to the caller. + +## Scope Limits + +Write only under the current task's `research/` directory. Do not edit code, specs, platform config, or task files outside research artifacts. diff --git a/.pi/extensions/trellis/index.ts b/.pi/extensions/trellis/index.ts new file mode 100644 index 0000000..b20a9e4 --- /dev/null +++ b/.pi/extensions/trellis/index.ts @@ -0,0 +1,1676 @@ +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { createHash, randomBytes } from "node:crypto"; +import { delimiter, dirname, isAbsolute, join, resolve } from "node:path"; +import { spawn, spawnSync } from "node:child_process"; + +// ── Types ────────────────────────────────────────────────────────────── +type JsonObject = Record<string, unknown>; +type TextContent = { type: "text"; text: string }; +interface PiToolResult { + content: TextContent[]; + details?: unknown; +} +interface PiExtensionContext { + hasUI?: boolean; + sessionManager?: { + getSessionId?: () => string; + getSessionFile?: () => string | undefined; + }; + ui?: { + notify?: (msg: string, type?: "info" | "warning" | "error") => void; + }; +} +interface SubagentInput { + agent?: string; + prompt?: string; + mode?: "single" | "parallel" | "chain"; + prompts?: string[]; + model?: string; + thinking?: string; +} +interface AgentConfig { + model?: string; + thinking?: string; + tools?: string[]; + fallbackModels: string[]; +} +interface PiRunConfig { + model?: string; + thinking?: string; + tools?: string[]; +} + +// ── Lazy-load pi-tui (avoid failing top-level imports) ───────────────── +let _piTui: { + visibleWidth?: (s: string) => number; + truncateToWidth?: (s: string, w: number, ellipsis?: string) => string; +} | null = null; +function piTui() { + if (!_piTui) { + try { + _piTui = require("@earendil-works/pi-tui"); + } catch { + _piTui = {}; + } + } + return _piTui; +} +function trunc(s: string, w: number) { + const t = piTui(); + return t.truncateToWidth + ? t.truncateToWidth(s, w, "…") + : s.length <= w + ? s + : w > 1 + ? s.slice(0, w - 1) + "…" + : s.slice(0, w); +} + +// ── Constants ───────────────────────────────────────────────────────── +const TRELLIS_AGENT_JSONL: Record<string, string> = { + "trellis-implement": "implement.jsonl", + implement: "implement.jsonl", + "trellis-check": "check.jsonl", + check: "check.jsonl", +}; +const MAX_STDOUT = 8 * 1024 * 1024; +const MAX_STDERR = 1024 * 1024; +const MAX_TAIL = 256 * 1024; +const MAX_LINE_BUFFER = 1024 * 1024; +const MAX_TOOL_ARG_CHARS = 2048; +const MAX_TOOLS = 256; +const MAX_PARALLEL_PROMPTS = 6; +const ABORT_KILL_GRACE_MS = 1500; +const SESSION_OVERVIEW_TIMEOUT_MS = 1500; +const THROTTLE_MS = 500; +const FIRST_REPLY_NOTICE = `<first-reply-notice> +First visible reply: say once in Chinese that Trellis SessionStart context is loaded, then answer directly. +This notice is one-shot: do not repeat it after the first assistant reply in the same session. +</first-reply-notice>`; + +// ── State types ─────────────────────────────────────────────────────── +type RunStatus = "pending" | "running" | "succeeded" | "failed" | "cancelled"; +type ToolStatus = "running" | "succeeded" | "failed"; + +interface Usage { + input: number; + output: number; + cacheRead: number; + cacheWrite: number; + cost: number; + ctxTokens: number; + turns: number; +} +interface ToolTrace { + id: string; + name: string; + args: string; + status: ToolStatus; + startedAt: number; + finishedAt?: number; +} +interface RunState { + id: string; + agent: string; + prompt: string; + step?: number; + status: RunStatus; + startedAt?: number; + finishedAt?: number; + finalText: string; + textTail: string; + thinkingTail: string; + stderrTail: string; + tools: ToolTrace[]; + usage: Usage; + model?: string; + thinking?: string; + errorMessage?: string; +} +interface ProgressDetails { + kind: "trellis-subagent-progress"; + agent: string; + mode: "single" | "parallel" | "chain"; + startedAt: number; + updatedAt: number; + final: boolean; + runs: RunState[]; +} + +// ── Native partial-update card state ────────────────────────────────── +interface NativeCardHandle { + state: JsonObject; + invalidate: () => void; + updatedAt: number; +} +const MAX_NATIVE_CARDS = 20; +const nativeCards = new Map<string, NativeCardHandle>(); +let activeSubagentToolCallId: string | null = null; +function rememberNativeCard(id: string, card: NativeCardHandle) { + nativeCards.set(id, card); + const active = activeSubagentToolCallId + ? nativeCards.get(activeSubagentToolCallId) + : undefined; + if (!active || card.updatedAt >= active.updatedAt) + activeSubagentToolCallId = id; + for (const key of nativeCards.keys()) { + if (nativeCards.size <= MAX_NATIVE_CARDS) break; + if (key !== activeSubagentToolCallId) nativeCards.delete(key); + } +} +function totalUsage(d: ProgressDetails): Usage { + const u: Usage = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + cost: 0, + ctxTokens: 0, + turns: 0, + }; + for (const r of d.runs) { + u.input += r.usage.input; + u.output += r.usage.output; + u.cacheRead += r.usage.cacheRead; + u.cacheWrite += r.usage.cacheWrite; + u.cost += r.usage.cost; + u.ctxTokens = Math.max(u.ctxTokens, r.usage.ctxTokens); + u.turns += r.usage.turns; + } + return u; +} +function activeRun(d: ProgressDetails) { + return d.runs.find((r) => r.status === "running") ?? d.runs.at(-1); +} +function toolArgs(t: ToolTrace) { + try { + return JSON.parse(t.args) as Record<string, unknown>; + } catch { + return {}; + } +} +function bashCommand(t: ToolTrace) { + const a = toolArgs(t); + return String(a.command || "").toLowerCase(); +} +function isSearchTool(t: ToolTrace) { + return t.name === "read" || t.name === "grep" || t.name === "find"; +} +function isMutationTool(t: ToolTrace) { + return t.name === "edit" || t.name === "write"; +} +function isValidationCommand(t: ToolTrace) { + const c = bashCommand(t); + return /\b(test|typecheck|lint|build|gofmt|go test|npm run|pnpm|vitest|tsc)\b/.test( + c, + ); +} +function isInspectionCommand(t: ToolTrace) { + const c = bashCommand(t); + return /\b(rg|grep|find|git diff|git status|ls|tree)\b/.test(c); +} +function thinkingIntent(text: string) { + const s = text.toLowerCase(); + if (/error|failed|failure|panic|exception|报错|失败|错误|异常/.test(s)) + return "Analyzing failure cause"; + if (/test|verify|check|typecheck|lint|验证|测试|检查/.test(s)) + return "Planning verification steps"; + if (/plan|approach|design|strategy|方案|计划|思路|设计/.test(s)) + return "Structuring the implementation approach"; + if (/implement|change|edit|modify|refactor|实现|修改|重构/.test(s)) + return "Reasoning through code changes"; + if (/inspect|search|locate|read|context|定位|搜索|阅读|上下文/.test(s)) + return "Locating relevant context"; + return ""; +} +function behaviorSummary(r: RunState) { + if (r.status === "succeeded") return "Task completed and result returned"; + if (r.status === "failed") + return "Task failed and error details were retained"; + + const runningTool = r.tools.findLast((t) => t.status === "running"); + if (runningTool) { + if (isMutationTool(runningTool)) return "Applying the plan to code"; + if (runningTool.name === "bash" && isValidationCommand(runningTool)) + return "Verifying whether the implementation passes"; + if (runningTool.name === "bash" && isInspectionCommand(runningTool)) + return "Inspecting current code state"; + if (isSearchTool(runningTool)) return "Locating relevant code and context"; + if (runningTool.name === "bash") + return "Validating assumptions with commands"; + return "Using tools to advance the task"; + } + + const recent = r.tools.slice(-5); + if (recent.some((t) => t.status === "failed")) + return "Investigating tool or command failure"; + if (recent.some(isMutationTool)) return "Reviewing recent changes"; + if (recent.some((t) => t.name === "bash" && isValidationCommand(t))) + return "Analyzing verification results"; + if ( + recent.length >= 2 && + recent.every( + (t) => isSearchTool(t) || (t.name === "bash" && isInspectionCommand(t)), + ) + ) + return "Mapping code structure and impact"; + + const intent = thinkingIntent(`${r.thinkingTail}\n${r.textTail}`); + if (intent) return intent; + if (!r.tools.length) return "Understanding the task and planning execution"; + return "Advancing the task and preparing next steps"; +} +function progressState(d: ProgressDetails) { + const running = d.runs.filter((r) => r.status === "running").length; + const failed = d.runs.some((r) => r.status === "failed"); + return failed + ? "failed" + : d.final + ? "completed" + : running + ? `${running} running` + : "pending"; +} +function progressDone(d: ProgressDetails) { + return d.runs.filter((r) => r.status !== "pending" && r.status !== "running") + .length; +} +function summaryText(text: string) { + return `${text.trim().replace(/[。.!?…]+$/u, "")}...`; +} +function splitModelThinking(model?: string, fallbackThinking?: string) { + const m = model?.match(/^(.*):(off|minimal|low|medium|high|xhigh)$/i); + return { + model: m ? m[1] : model, + thinking: (m?.[2] ?? fallbackThinking)?.toLowerCase(), + }; +} +function modelLabel(r: RunState) { + const { model, thinking } = splitModelThinking(r.model, r.thinking); + if (!model) return undefined; + return thinking && thinking !== "off" ? `${model}(${thinking})` : model; +} +function applyRunConfig(r: RunState, cfg: PiRunConfig) { + const parsed = splitModelThinking(cfg.model, cfg.thinking); + r.model = parsed.model; + r.thinking = parsed.thinking; +} +function runElapsed(d: ProgressDetails, r: RunState) { + const start = r.startedAt ?? d.startedAt; + const end = + r.finishedAt ?? (r.status === "running" ? Date.now() : d.updatedAt); + return fmtDur(Math.max(0, end - start)); +} +function runHeader(d: ProgressDetails, r: RunState) { + const usage = fmtUsage(r.usage, modelLabel(r)) || fmtUsage(totalUsage(d)); + return `${r.agent} · ${progressDone(d)}/${d.runs.length} done · ${progressState(d)} · ${runElapsed(d, r)}${usage ? ` · ${usage}` : ""}`; +} +function renderRunBlock( + lines: string[], + d: ProgressDetails, + run: RunState, + expanded: boolean, +) { + const step = run.step ? `step ${run.step} · ` : ""; + lines.push(` - ${step}${runHeader(d, run)}`); + const summary = behaviorSummary(run); + if (summary) lines.push(` › ${summaryText(summary)}`); + const visibleTools = expanded ? run.tools.slice(-8) : run.tools.slice(-1); + for (const t of visibleTools) + lines.push(` ${toolIcon(t.status)} ${toolBrief(t)}`); + if (expanded && run.errorMessage) { + lines.push(` ✗ ${oneLine(run.errorMessage, 120)}`); + } +} +function renderProgressCard( + d: ProgressDetails, + expanded: boolean, + w: number, +): string[] { + const r = activeRun(d); + if (!r) return []; + const spinner = ["◐", "◓", "◑", "◒"][Math.floor(Date.now() / 250) % 4]!; + const icon = d.final + ? d.runs.some((x) => x.status === "failed") + ? "✗" + : "✓" + : spinner; + const totalElapsed = fmtDur( + (d.final ? d.updatedAt : Date.now()) - d.startedAt, + ); + const lines: string[] = [ + `${icon} subagent ${d.mode} · total ${totalElapsed}`, + ]; + + if (!expanded) { + renderRunBlock(lines, d, r, false); + lines.push(" Alt+O expand latest subagent card"); + return lines.map((l) => trunc(l, w)); + } + + for (const run of d.runs) renderRunBlock(lines, d, run, true); + lines.push(" Alt+O collapse latest subagent card"); + const max = 48; + const shown = + lines.length > max + ? [ + ...lines.slice(0, max - 1), + ` … ${lines.length - max + 1} lines hidden`, + ] + : lines; + return shown.map((l) => trunc(l, w)); +} +function progressKey(d: ProgressDetails) { + return d.runs + .map((r) => { + const t = r.tools.at(-1); + return [ + r.id, + r.status, + r.tools.length, + t?.id ?? "", + t?.status ?? "", + r.usage.turns, + r.usage.input, + r.usage.output, + r.usage.cacheRead, + r.usage.cacheWrite, + r.usage.ctxTokens, + r.model ?? "", + r.thinking ?? "", + r.errorMessage ?? "", + ].join("~"); + }) + .join("|"); +} + +// ── Utilities ───────────────────────────────────────────────────────── +function isObj(v: unknown): v is JsonObject { + return typeof v === "object" && v !== null && !Array.isArray(v); +} +function str(v: unknown): string | null { + return typeof v === "string" && v.trim() ? v.trim() : null; +} +function num(v: unknown): number { + return typeof v === "number" && Number.isFinite(v) ? v : 0; +} +function hash(s: string) { + return createHash("sha256").update(s).digest("hex").slice(0, 24); +} +function readText(p: string) { + try { + return readFileSync(p, "utf-8"); + } catch { + return ""; + } +} +function exists(p: string) { + try { + return statSync(p).isFile(); + } catch { + return false; + } +} +function shellQuote(v: string) { + return `'${v.replace(/'/g, `'\\''`)}'`; +} +function callStr(cb: (() => string | undefined) | undefined): string | null { + if (!cb) return null; + try { + return str(cb()); + } catch { + return null; + } +} +function lookupStr(data: unknown, keys: string[]): string | null { + if (!isObj(data)) return null; + for (const k of keys) { + const v = str(data[k]); + if (v) return v; + } + for (const nk of [ + "input", + "properties", + "event", + "hook_input", + "hookInput", + ]) { + const nested = data[nk]; + const v = lookupStr(nested, keys); + if (v) return v; + } + return null; +} +function cmdHasTrellisCtx(cmd: string) { + const t = cmd.trimStart(); + return ( + /^export\s+TRELLIS_CONTEXT_ID=/.test(t) || + /^TRELLIS_CONTEXT_ID=/.test(t) || + /^env\s+.*TRELLIS_CONTEXT_ID=/.test(t) + ); +} +function fmtDur(ms: number) { + if (ms < 1000) return `${ms}ms`; + const s = Math.floor(ms / 1000); + if (s < 60) return `${s}s`; + return `${Math.floor(s / 60)}m${s % 60}s`; +} +function fmtNum(n: number) { + if (!n) return "0"; + if (Math.abs(n) < 1000) return `${n}`; + if (Math.abs(n) < 1000000) return `${(n / 1000).toFixed(1)}k`; + return `${(n / 1000000).toFixed(1)}m`; +} +function fmtUsage(u: Usage, m?: string) { + const p: string[] = []; + if (u.turns) p.push(`${u.turns}t`); + if (u.input) p.push(`↑${fmtNum(u.input)}`); + if (u.output) p.push(`↓${fmtNum(u.output)}`); + if (u.cost) p.push(`$${u.cost.toFixed(3)}`); + if (u.ctxTokens) p.push(`ctx:${fmtNum(u.ctxTokens)}`); + if (m) p.push(m); + return p.join(" "); +} +function statusIcon(s: RunStatus) { + return s === "pending" + ? "○" + : s === "running" + ? "●" + : s === "succeeded" + ? "✓" + : s === "failed" + ? "✗" + : "⊘"; +} +function toolIcon(s: ToolStatus) { + return s === "running" ? "•" : s === "succeeded" ? "✓" : "✗"; +} +function latest(text: string, n: number) { + return text + .split(/\r?\n/) + .map((l) => l.trimEnd()) + .filter((l) => l.trim()) + .slice(-n); +} +function appendTail(cur: string, next: string, max: number) { + if (!next) return cur; + const c = cur + next; + return c.length <= max ? c : c.slice(-max); +} +function extractText(content: unknown): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content + .map((b) => + isObj(b) && b.type === "text" && typeof b.text === "string" ? b.text : "", + ) + .join(""); +} +function extractThinking(content: unknown): string { + if (!Array.isArray(content)) return ""; + return content + .map((b) => + isObj(b) && b.type === "thinking" && typeof b.thinking === "string" + ? b.thinking + : "", + ) + .join("\n"); +} +function newUsage(): Usage { + return { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + cost: 0, + ctxTokens: 0, + turns: 0, + }; +} +function newRun( + id: string, + agent: string, + prompt: string, + step?: number, +): RunState { + return { + id, + agent, + prompt: trunc(prompt.replace(/\s+/g, " ").trim(), 120) || "(empty)", + step, + status: "pending", + finalText: "", + textTail: "", + thinkingTail: "", + stderrTail: "", + tools: [], + usage: newUsage(), + }; +} +function cloneProgress(d: ProgressDetails): ProgressDetails { + return { + ...d, + runs: d.runs.map((r) => ({ + ...r, + tools: r.tools.map((t) => ({ ...t })), + usage: { ...r.usage }, + })), + }; +} + +function oneLine(v: unknown, max = 80) { + return String(v || "...") + .replace(/\s+/g, " ") + .trim() + .slice(0, max); +} +function summarizeToolArgs(name: string, args: unknown): string { + const a = isObj(args) ? args : {}; + const summary: JsonObject = {}; + if ("path" in a) summary.path = oneLine(a.path, 240); + if ("file_path" in a) summary.file_path = oneLine(a.file_path, 240); + if ("command" in a) summary.command = oneLine(a.command, 240); + if ("pattern" in a) summary.pattern = oneLine(a.pattern, 120); + if ("limit" in a) summary.limit = a.limit; + if ("offset" in a) summary.offset = a.offset; + if (name === "edit" && Array.isArray(a.edits)) + summary.edits = `${a.edits.length} edit(s)`; + if (name === "write" && "content" in a) + summary.content = `<${String(a.content ?? "").length} chars>`; + const json = JSON.stringify( + Object.keys(summary).length ? summary : { tool: name }, + ); + return json.length <= MAX_TOOL_ARG_CHARS + ? json + : json.slice(0, MAX_TOOL_ARG_CHARS); +} +function toolBrief(t: ToolTrace): string { + const a = toolArgs(t); + if (t.name === "read") return `read: ${oneLine(a.path || a.file_path, 80)}`; + if (t.name === "bash") return `bash: ${oneLine(a.command, 60)}`; + if (t.name === "write") return `write: ${oneLine(a.path || a.file_path, 80)}`; + if (t.name === "edit") return `edit: ${oneLine(a.path || a.file_path, 80)}`; + if (t.name === "grep") return `grep: ${oneLine(a.pattern, 50)}`; + if (t.name === "find") return `find: ${oneLine(a.pattern || "*", 50)}`; + return oneLine(t.name, 50); +} + +// ── Pi CLI path resolution ──────────────────────────────────────────── +const PI_CLI_SEGMENTS = [ + ["node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js"], + ["node_modules", "@mariozechner", "pi-coding-agent", "dist", "cli.js"], +]; + +function resolvePiCli(): { command: string; args: string[] } { + const envCli = str(process.env.TRELLIS_PI_CLI_JS); + if (envCli) { + const p = resolve(envCli); + if (!exists(p)) throw new Error(`TRELLIS_PI_CLI_JS missing: ${p}`); + return { command: process.execPath, args: [p] }; + } + const candidates: string[] = []; + for (const arg of process.argv) + if (/pi-coding-agent[\\/]dist[\\/]cli\.js$/i.test(arg)) + candidates.push(resolve(arg)); + const prefix = + str(process.env.npm_config_prefix) ?? str(process.env.NPM_CONFIG_PREFIX); + const appData = str(process.env.APPDATA); + const pathVal = process.env.PATH ?? process.env.Path ?? ""; + const addBase = (base: string) => { + for (const seg of PI_CLI_SEGMENTS) candidates.push(join(base, ...seg)); + }; + if (prefix) { + addBase(prefix); + addBase(join(prefix, "lib")); + } + if (appData) addBase(join(appData, "npm")); + for (const entry of pathVal.split(delimiter)) { + const e = entry.trim(); + if (!e) continue; + addBase(e); + addBase(dirname(e)); + addBase(join(dirname(e), "lib")); + } + for (const c of [...new Set(candidates)]) + if (exists(c)) return { command: process.execPath, args: [c] }; + return { command: "pi", args: [] }; +} + +function resolveRunCfg( + input: SubagentInput, + agentCfg: AgentConfig, + inheritedThinking?: string, +): PiRunConfig { + const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"]; + const normalize = (v: unknown): string | undefined => { + const s = typeof v === "string" && v.trim() ? v.trim().toLowerCase() : ""; + return THINKING_LEVELS.includes(s) ? s : undefined; + }; + const suffixRe = /:(off|minimal|low|medium|high|xhigh)$/i; + const inputModel = str(input.model); + const agentModel = agentCfg.model; + const rawModel = inputModel ?? agentModel; + const inputSuffixThinking = normalize(inputModel?.match(suffixRe)?.[1]); + const agentSuffixThinking = normalize(agentModel?.match(suffixRe)?.[1]); + const baseModel = rawModel?.replace(suffixRe, ""); + const thinking = + normalize(input.thinking) ?? + inputSuffixThinking ?? + normalize(agentCfg.thinking) ?? + agentSuffixThinking ?? + normalize(inheritedThinking); + if (baseModel && thinking && thinking !== "off") + return { model: `${baseModel}:${thinking}`, thinking, tools: agentCfg.tools }; + return { model: baseModel || rawModel, thinking, tools: agentCfg.tools }; +} + +function buildPiArgs(cfg: PiRunConfig): string[] { + const args = ["--mode", "json", "-p", "--no-session"]; + if (cfg.model) + args.push( + "--model", + cfg.thinking && cfg.thinking !== "off" && !cfg.model.includes(":") + ? `${cfg.model}:${cfg.thinking}` + : cfg.model, + ); + else if (cfg.thinking && cfg.thinking !== "off") + args.push("--thinking", cfg.thinking); + if (cfg.tools && cfg.tools.length > 0) + args.push("--tools", cfg.tools.join(",")); + return args; +} + +// ── BoundedBufferCollector ───────────────────────────────────────────── +class BBC { + private c: Buffer[] = []; + private len = 0; + private trunc = 0; + constructor(private max: number) {} + append(b: Buffer) { + if (b.length >= this.max) { + this.trunc += this.len + b.length - this.max; + this.c = [b.subarray(b.length - this.max)]; + this.len = this.max; + return; + } + this.c.push(b); + this.len += b.length; + while (this.len > this.max) { + const f = this.c[0]!; + if (f.length <= this.len - this.max) { + this.c.shift(); + this.len -= f.length; + this.trunc += f.length; + } else { + const ov = this.len - this.max; + this.c[0] = f.subarray(ov); + this.len -= ov; + this.trunc += ov; + break; + } + } + } + toString() { + const body = Buffer.concat(this.c, this.len).toString("utf-8"); + return this.trunc ? `[${this.trunc} bytes truncated]\n${body}` : body; + } +} + +// ── Trellis Context ──────────────────────────────────────────────────── +function findRoot(start: string): string { + let c = resolve(start); + while (true) { + if (existsSync(join(c, ".trellis")) || existsSync(join(c, ".pi"))) return c; + const p = dirname(c); + if (p === c) return resolve(start); + c = p; + } +} +function splitFM(c: string) { + const m = c.replace(/^\uFEFF/, "").match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + return m + ? { fm: m[1] ?? "", body: c.slice(m[0].length) } + : { fm: "", body: c }; +} +function stripFM(c: string) { + return splitFM(c).body.trimStart(); +} +function parseAgentFM(c: string): AgentConfig { + const cfg: AgentConfig = { fallbackModels: [] }; + const { fm } = splitFM(c); + const lines = fm.split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + const m = (lines[i] ?? "").match(/^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/); + if (!m) continue; + const k = m[1] ?? "", + v = m[2] ?? ""; + if (k === "model") + cfg.model = v.trim().replace(/^["']|["']$/g, "") || undefined; + else if (k === "thinking") + cfg.thinking = (v.trim().replace(/^["']|["']$/g, "") || undefined) as + | string + | undefined; + else if (k === "fallbackModels" || k === "fallback_models") { + if (v.trim()) { + cfg.fallbackModels = v + .trim() + .replace(/^\[|\]$/g, "") + .split(",") + .map((s) => s.trim().replace(/^["']|["']$/g, "")) + .filter(Boolean); + } else { + i++; + while (i < lines.length && /^\s+-\s/.test(lines[i] ?? "")) { + const item = (lines[i] ?? "") + .trim() + .replace(/^-\s+/, "") + .replace(/^["']|["']$/g, ""); + if (item) cfg.fallbackModels.push(item); + i++; + } + i--; + } + } else if (k === "tools") { + // Pi tool names are lowercase (read, bash, edit, write, grep, find, ls). + // Normalize to lowercase so mixed-case frontmatter still matches. + if (v.trim()) { + cfg.tools = v + .trim() + .split(",") + .map((s) => s.trim().replace(/^["']|["']$/g, "").toLowerCase()) + .filter(Boolean); + } + } + } + return cfg; +} + +function contextKey(input?: unknown, ctx?: PiExtensionContext): string | null { + const ov = str(process.env.TRELLIS_CONTEXT_ID); + if (ov) return ov.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 160) || hash(ov); + const sessionId = + callStr(ctx?.sessionManager?.getSessionId) ?? + str(process.env.PI_SESSION_ID) ?? + str(process.env.PI_SESSIONID) ?? + lookupStr(input, ["session_id", "sessionId", "sessionID"]); + if (sessionId) + return `pi_${sessionId.replace(/[^A-Za-z0-9._-]+/g, "_") || hash(sessionId)}`; + const transcriptPath = + callStr(ctx?.sessionManager?.getSessionFile) ?? + lookupStr(input, ["transcript_path", "transcriptPath", "transcript"]); + if (transcriptPath) return `pi_transcript_${hash(transcriptPath)}`; + return null; +} + +function readTaskDir(root: string, key: string | null): string | null { + if (!key) return null; + try { + const ctx = JSON.parse( + readText(join(root, ".trellis", ".runtime", "sessions", `${key}.json`)), + ) as JsonObject; + let ref = str(ctx.current_task); + if (!ref) return null; + ref = ref; + ref = ref.replace(/\\/g, "/").replace(/^\.\//, ""); + if (ref.startsWith("tasks/")) ref = `.trellis/${ref}`; + return ref.startsWith(".trellis/") + ? join(root, ref) + : isAbsolute(ref) + ? ref + : join(root, ".trellis", "tasks", ref); + } catch { + return null; + } +} +function sessionHasTask(root: string, key: string): boolean { + try { + const ctx = JSON.parse( + readText(join(root, ".trellis", ".runtime", "sessions", `${key}.json`)), + ) as JsonObject; + return !!str(ctx.current_task); + } catch { + return false; + } +} +function adoptKey(root: string, key: string): string { + if (sessionHasTask(root, key)) return key; + try { + const dir = join(root, ".trellis", ".runtime", "sessions"); + const keys = readdirSync(dir) + .filter( + (f) => f.endsWith(".json") && sessionHasTask(root, f.slice(0, -5)), + ) + .map((f) => f.slice(0, -5)); + const proc = keys.filter((k) => k.startsWith("pi_process_")); + const cands = proc.length ? proc : keys; + return cands.length === 1 ? cands[0]! : key; + } catch { + return key; + } +} + +// ── Workflow State Breadcrumb ───────────────────────────────────────── +const WF_RE = + /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g; +function workflowBreadcrumb(root: string, key: string | null): string { + const wf = readText(join(root, ".trellis", "workflow.md")); + if (!wf) return ""; + const templates: Record<string, string> = {}; + for (const m of wf.matchAll(WF_RE)) { + const s = m[1] ?? "", + b = (m[2] ?? "").trim(); + if (s && b) templates[s] = b; + } + const dir = readTaskDir(root, key); + let header = "Status: no_task", + lookup = "no_task"; + if (dir) { + try { + const d = JSON.parse(readText(join(dir, "task.json"))) as JsonObject; + const status = str(d.status) ?? ""; + const id = str(d.id) ?? dir.split(/[\\/]/).pop() ?? ""; + if (status) { + header = `Task: ${id} (${status})`; + lookup = status; + } + } catch {} + } + const body = templates[lookup] ?? "Refer to workflow.md for current step."; + return `<workflow-state>\n${header}\n${body}\n</workflow-state>`; +} + +// ── Session Overview ─────────────────────────────────────────────────── +function runContextScript(root: string, key: string | null, args: string[]): string { + const script = join(root, ".trellis", "scripts", "get_context.py"); + if (!exists(script)) return ""; + try { + const py = process.platform === "win32" ? "python" : "python3"; + const result = spawnSync(py, [script, ...args], { + cwd: root, + env: key ? { ...process.env, TRELLIS_CONTEXT_ID: key } : process.env, + encoding: "utf-8", + timeout: SESSION_OVERVIEW_TIMEOUT_MS, + windowsHide: true, + }); + if (result.status !== 0) return ""; + const stdout = (result.stdout ?? "").trim(); + return stdout; + } catch { + return ""; + } +} + +function sessionOverview(root: string, key: string | null): string { + const stdout = runContextScript(root, key, []); + return stdout ? `<session-overview>\n${stdout}\n</session-overview>` : ""; +} + +function workflowOverview(root: string, key: string | null): string { + const stdout = runContextScript(root, key, [ + "--mode", + "phase", + "--platform", + "pi", + ]); + return stdout ? `<trellis-workflow>\n${stdout}\n</trellis-workflow>` : ""; +} + +function buildStartupContext( + root: string, + key: string | null, + overview: string, +): string { + const workflow = workflowOverview(root, key); + return [ + "<session-context>\nTrellis compact SessionStart context. Use it to orient the session; load details on demand.\n</session-context>", + FIRST_REPLY_NOTICE, + overview, + workflow, + "<ready>\nUse the current workflow state to decide whether to create, continue, or skip a Trellis task.\n</ready>", + ] + .filter(Boolean) + .join("\n\n"); +} + +function buildContext(root: string, agent: string, key: string | null): string { + const dir = readTaskDir(root, key); + if (!dir) + return "No active Trellis task found. Read .trellis/ before proceeding."; + const prd = readText(join(dir, "prd.md")); + const design = readText(join(dir, "design.md")); + const impl = readText(join(dir, "implement.md")); + const jsonlName = TRELLIS_AGENT_JSONL[agent] ?? ""; + let spec = ""; + if (jsonlName) { + const chunks: string[] = []; + for (const line of readText(join(dir, jsonlName)).split(/\r?\n/)) { + const t = line.trim(); + if (!t) continue; + try { + const r = JSON.parse(t) as JsonObject; + const f = typeof r.file === "string" ? r.file : ""; + if (f) { + const c = readText(join(root, f)); + if (c) chunks.push(`## ${f}\n\n${c}`); + } + } catch {} + } + spec = chunks.join("\n\n---\n\n"); + } + return [ + `## Trellis Task Context`, + `Task directory: ${dir}`, + "", + "### prd.md", + prd || "(missing)", + design ? "\n### design.md\n" + design : "", + impl ? "\n### implement.md\n" + impl : "", + spec ? "\n### Curated Spec / Research Context\n" + spec : "", + ].join("\n"); +} + +function normalizeAgent(agent: string | undefined): string { + const name = agent ?? "trellis-implement"; + return name.startsWith("trellis-") ? name : `trellis-${name}`; +} + +function isTrellisAgent(root: string, agent: string): boolean { + return existsSync(join(root, ".pi", "agents", `${agent}.md`)); +} + +function buildPrompt( + root: string, + input: SubagentInput, + key: string | null, +): string { + const agent = normalizeAgent(input.agent); + const raw = readText(join(root, ".pi", "agents", `${agent}.md`)); + const def = stripFM(raw); + const ctx = buildContext(root, agent, key); + return [ + "## Trellis Agent Definition", + def || "(missing)", + "", + ctx, + "", + "## Delegated Task", + input.prompt ?? "", + ].join("\n"); +} + +// ── Event parsing ───────────────────────────────────────────────────── +function parseJsonEvent(line: string): JsonObject | null { + const t = line.trim(); + if (!t) return null; + const i = t.indexOf("{"); + if (i < 0) return null; + try { + const p = JSON.parse(t.slice(i)); + return isObj(p) ? p : null; + } catch { + return null; + } +} + +function applyEvent(r: RunState, evt: JsonObject): boolean { + const type = typeof evt.type === "string" ? evt.type : ""; + if (!type) return false; + if (type === "agent_start" || type === "turn_start") { + r.status = "running"; + r.startedAt ??= Date.now(); + return true; + } + if (type === "message_update") { + const ae = isObj(evt.assistantMessageEvent) + ? evt.assistantMessageEvent + : null; + if (!ae || typeof ae.delta !== "string") return false; + if (ae.type === "thinking_delta") { + r.thinkingTail = appendTail(r.thinkingTail, ae.delta, MAX_TAIL); + return true; + } + if (ae.type === "text_delta") { + r.textTail = appendTail(r.textTail, ae.delta, MAX_TAIL); + return true; + } + return false; + } + if (type === "message_end" && isObj(evt.message)) { + const msg = evt.message; + if (msg.role !== "assistant") return false; + r.usage.turns += 1; + const u = isObj(msg.usage) ? msg.usage : null; + const cost = isObj(u?.cost) ? u.cost : null; + r.usage.input += num(u?.input); + r.usage.output += num(u?.output); + r.usage.cacheRead += num(u?.cacheRead); + r.usage.cacheWrite += num(u?.cacheWrite); + r.usage.cost += num(cost?.total); + r.usage.ctxTokens = num(u?.totalTokens); + const thinking = extractThinking(msg.content); + if (thinking) r.thinkingTail = appendTail("", thinking, MAX_TAIL); + const text = extractText(msg.content); + if (text) { + r.finalText = text; + r.textTail = appendTail("", text, MAX_TAIL); + } + if (typeof msg.model === "string") { + const parsed = splitModelThinking(msg.model, r.thinking); + r.model = parsed.model; + r.thinking = parsed.thinking; + } + if (typeof msg.errorMessage === "string") r.errorMessage = msg.errorMessage; + return true; + } + if (type === "tool_execution_start") { + const id = + typeof evt.toolCallId === "string" + ? evt.toolCallId + : hash(`${Date.now()}`); + const name = typeof evt.toolName === "string" ? evt.toolName : "tool"; + const args = summarizeToolArgs(name, evt.args); + const existing = r.tools.findIndex((t) => t.id === id); + if (existing >= 0) + r.tools[existing] = { ...r.tools[existing]!, args, status: "running" }; + else + r.tools.push({ + id, + name, + args, + status: "running", + startedAt: Date.now(), + }); + if (r.tools.length > MAX_TOOLS) + r.tools.splice(0, r.tools.length - MAX_TOOLS); + return true; + } + if (type === "tool_execution_end") { + const id = typeof evt.toolCallId === "string" ? evt.toolCallId : ""; + const idx = r.tools.findIndex((t) => t.id === id); + if (idx >= 0) + r.tools[idx] = { + ...r.tools[idx]!, + status: evt.isError ? "failed" : "succeeded", + finishedAt: Date.now(), + }; + return true; + } + if (type === "agent_end") { + r.finishedAt = Date.now(); + if (r.status === "running" || r.status === "pending") + r.status = "succeeded"; + return true; + } + return false; +} + +function finalize(r: RunState, fallback: string): string { + return r.finalText || fallback.trim() || r.stderrTail.trim(); +} +function formatPiOutput(stdout: string, stderr: string): string { + let ft = ""; + for (const line of stdout.split(/\r?\n/)) { + const t = line.trim(); + if (!t) continue; + try { + const evt = JSON.parse(t) as JsonObject; + const msg = isObj(evt.message) ? evt.message : null; + if (msg?.role === "assistant") { + const txt = extractText(msg.content); + if (txt) ft = txt; + } + } catch {} + } + return ft || stdout || stderr; +} + +// ── runPi: subprocess execution + event processing ─────────────────── +function runPi( + root: string, + prompt: string, + cfg: PiRunConfig, + state: RunState, + emit: () => void, + key?: string | null, + signal?: AbortSignal, +): Promise<{ output: string; failed: boolean }> { + return new Promise((resolve) => { + if (signal?.aborted) { + state.status = "cancelled"; + state.errorMessage = "cancelled"; + state.finishedAt = Date.now(); + emit(); + resolve({ output: "cancelled", failed: true }); + return; + } + const inv = resolvePiCli(); + const childEnv = { + ...process.env, + TRELLIS_SUBAGENT_CHILD: "1", + ...(key ? { TRELLIS_CONTEXT_ID: key } : {}), + }; + const cli = spawn(inv.command, [...inv.args, ...buildPiArgs(cfg)], { + cwd: root, + env: childEnv, + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, + }); + const stdout = new BBC(MAX_STDOUT); + const stderr = new BBC(MAX_STDERR); + let buf = ""; + let settled = false; + let aborted = false; + let killTimer: ReturnType<typeof setTimeout> | null = null; + const abort = () => { + aborted = true; + cli.kill(); + killTimer = setTimeout(() => { + if (!settled && cli.exitCode === null) cli.kill("SIGKILL"); + }, ABORT_KILL_GRACE_MS); + killTimer?.unref?.(); + }; + const done = (v: { output: string; failed: boolean }) => { + if (settled) return; + settled = true; + if (killTimer) clearTimeout(killTimer); + signal?.removeEventListener("abort", abort); + emit(); + resolve(v); + }; + signal?.addEventListener("abort", abort, { once: true }); + state.status = "running"; + state.startedAt = Date.now(); + emit(); + const processLine = (line: string) => { + const evt = parseJsonEvent(line); + if (evt && applyEvent(state, evt)) emit(); + }; + cli.stdout?.on("data", (d: Buffer) => { + stdout.append(d); + buf += d.toString("utf-8"); + if (buf.length > MAX_LINE_BUFFER) buf = buf.slice(-MAX_LINE_BUFFER); + const lines = buf.split(/\r?\n/); + buf = lines.pop() ?? ""; + for (const l of lines) processLine(l); + }); + cli.stderr?.on("data", (d: Buffer) => { + stderr.append(d); + state.stderrTail = appendTail( + state.stderrTail, + d.toString("utf-8"), + MAX_TAIL, + ); + }); + cli.stdin?.on("error", (e: Error & { code?: string }) => { + if (!aborted && e.code !== "EPIPE") + done({ output: e.message, failed: true }); + }); + cli.on("error", (e) => { + state.status = aborted ? "cancelled" : "failed"; + state.errorMessage = e instanceof Error ? e.message : String(e); + state.finishedAt = Date.now(); + done({ output: finalize(state, state.errorMessage), failed: true }); + }); + cli.on("close", (code) => { + if (buf.trim()) processLine(buf); + const out = stdout.toString(); + const err = stderr.toString(); + state.stderrTail = appendTail("", err, MAX_TAIL); + state.finishedAt = Date.now(); + if (aborted) { + state.status = "cancelled"; + state.errorMessage = "cancelled"; + done({ output: finalize(state, "cancelled"), failed: true }); + return; + } + if (code === 0) { + if (state.status === "pending" || state.status === "running") + state.status = "succeeded"; + done({ + output: finalize(state, formatPiOutput(out, err)), + failed: false, + }); + return; + } + state.status = "failed"; + state.errorMessage = err || out || `exit ${code ?? "?"}`; + done({ output: finalize(state, state.errorMessage), failed: true }); + }); + cli.stdin?.end(prompt); + }); +} + +// ── runSubagent: orchestrate single/parallel/chain via native partial updates ── +async function runSubagent( + root: string, + input: SubagentInput, + key: string | null, + signal?: AbortSignal, + onUpdate?: (r: PiToolResult) => void, + inheritedThinking?: string, +): Promise<{ output: string; details: ProgressDetails; failed: boolean }> { + const agentName = normalizeAgent(input.agent); + const agentRaw = readText(join(root, ".pi", "agents", `${agentName}.md`)); + const agentCfg = parseAgentFM(agentRaw); + const runCfg = resolveRunCfg(input, agentCfg, inheritedThinking); + const mode = input.mode ?? "single"; + const startedAt = Date.now(); + const details: ProgressDetails = { + kind: "trellis-subagent-progress", + agent: agentName, + mode, + startedAt, + updatedAt: startedAt, + final: false, + runs: [], + }; + let lastEmit = 0; + let lastPartialKey = ""; + let closed = false; + const pushPartial = (force = false) => { + if (closed || !onUpdate) return; + const key = progressKey(details); + if (!force && key === lastPartialKey) return; + lastPartialKey = key; + onUpdate({ + // Keep native partial content stable; renderResult owns the visible progress UI. + content: [{ type: "text", text: "subagent running" }], + details: cloneProgress(details), + }); + }; + const emit = (force = false) => { + const now = Date.now(); + if (!force && now - lastEmit < THROTTLE_MS) return; + lastEmit = now; + details.updatedAt = now; + pushPartial(force); + }; + const finish = (output: string, failed: boolean) => { + closed = true; + details.final = true; + details.updatedAt = Date.now(); + return { output, details: cloneProgress(details), failed }; + }; + + try { + if (mode === "parallel") { + const prompts = input.prompts ?? (input.prompt ? [input.prompt] : []); + details.runs = prompts.map((p, i) => { + const r = newRun(`${agentName}-${i + 1}`, agentName, p); + applyRunConfig(r, runCfg); + return r; + }); + emit(true); + const results = await Promise.all( + prompts.map((p, i) => + runPi( + root, + buildPrompt(root, { ...input, prompt: p }, key), + runCfg, + details.runs[i]!, + emit, + key, + signal, + ), + ), + ); + return finish( + results.map((r) => r.output).join("\n\n---\n\n"), + results.some((r) => r.failed), + ); + } + if (mode === "chain") { + let prev = ""; + let failed = false; + for (let i = 0; i < (input.prompts?.length ?? 1); i++) { + const p = input.prompts?.[i] ?? input.prompt ?? ""; + const rs = newRun(`${agentName}-${i + 1}`, agentName, p, i + 1); + applyRunConfig(rs, runCfg); + details.runs.push(rs); + emit(true); + const result = await runPi( + root, + buildPrompt( + root, + { + ...input, + prompt: prev ? `${p}\n\nPrevious output:\n${prev}` : p, + }, + key, + ), + runCfg, + rs, + emit, + key, + signal, + ); + prev = result.output; + failed = failed || result.failed; + if (result.failed) break; + } + return finish(prev, failed); + } + const rs = newRun(`${agentName}-1`, agentName, input.prompt ?? ""); + applyRunConfig(rs, runCfg); + details.runs = [rs]; + emit(true); + const result = await runPi( + root, + buildPrompt(root, input, key), + runCfg, + rs, + emit, + key, + signal, + ); + return finish(result.output, result.failed); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + const r = activeRun(details); + if (r) { + r.status = "failed"; + r.errorMessage = message; + r.finishedAt = Date.now(); + } + return finish(message, true); + } +} + +// ── Extension ────────────────────────────────────────────────────────── +export default function trellisExtension(pi: { + registerTool?: (tool: JsonObject) => void; + registerShortcut?: ( + key: string, + opts: { + description?: string; + handler: (ctx: PiExtensionContext) => unknown; + }, + ) => void; + on?: ( + event: string, + handler: (event: unknown, ctx?: PiExtensionContext) => unknown, + ) => void; + getThinkingLevel?: () => string; +}): void { + if (process.env.TRELLIS_SUBAGENT_CHILD === "1") return; + const root = findRoot(process.cwd()); + const procKey = `pi_process_${hash([root, process.pid, Date.now(), randomBytes(8).toString("hex")].join(":"))}`; + let curKey: string | null = null; + + const getKey = (input?: unknown, ctx?: PiExtensionContext) => { + const k = adoptKey(root, contextKey(input, ctx) ?? curKey ?? procKey); + curKey = k; + return k; + }; + + // Per-turn cache to avoid double-spawning python + let turnCache: { + key: string | null; + ts: number; + wf: string; + ov: string; + } | null = null; + const getTurnCtx = (k: string | null) => { + const now = Date.now(); + if (turnCache && turnCache.key === k && now - turnCache.ts < 1500) + return turnCache; + turnCache = { + key: k, + ts: now, + wf: workflowBreadcrumb(root, k), + ov: sessionOverview(root, k), + }; + return turnCache; + }; + const startupKeys = new Set<string>(); + const getStartupCtx = ( + k: string | null, + turn: { ov: string }, + ): string => { + const key = k ?? "default"; + if (startupKeys.has(key)) return ""; + startupKeys.add(key); + return buildStartupContext(root, k, turn.ov); + }; + + // Toggle only the latest subagent native card; do not use Pi global tool expansion. + const toggleDetail = (ctx: PiExtensionContext) => { + const id = activeSubagentToolCallId; + const card = id ? nativeCards.get(id) : undefined; + if (!card) { + ctx.ui?.notify?.("No subagent card to toggle yet.", "warning"); + return; + } + card.state.localExpanded = card.state.localExpanded !== true; + card.invalidate(); + }; + + pi.registerShortcut?.("alt+o", { + description: "Toggle latest subagent card details", + handler: async (ctx: PiExtensionContext) => toggleDetail(ctx), + }); + + // Tool registration + pi.registerTool?.({ + name: "trellis_subagent", + label: "Trellis Subagent", + description: "Run a Trellis project sub-agent with active task context.", + promptSnippet: + 'Sub-agent dispatch protocol (Trellis): your dispatch prompt MUST start with one line "Active task: <task path from `task.py current`>" before any other instructions.', + promptGuidelines: [ + 'Use subagent for task delegation. Your dispatch prompt MUST start with "Active task: <task path from `task.py current`>".', + ], + parameters: { + type: "object", + properties: { + agent: { + type: "string", + description: + "Agent name, such as trellis-implement or trellis-check.", + }, + prompt: { + type: "string", + description: "Task prompt for the sub-agent.", + }, + mode: { type: "string", enum: ["single", "parallel", "chain"] }, + prompts: { + type: "array", + items: { type: "string" }, + maxItems: MAX_PARALLEL_PROMPTS, + }, + model: { + type: "string", + description: + "Optional Pi model override for the child sub-agent process.", + }, + thinking: { + type: "string", + description: + "Optional Pi thinking level override for the child sub-agent process.", + enum: ["off", "minimal", "low", "medium", "high", "xhigh"], + }, + }, + }, + execute: async ( + id: string, + input: SubagentInput, + signal?: AbortSignal, + onUpdate?: (r: PiToolResult) => void, + ctx?: PiExtensionContext, + ) => { + activeSubagentToolCallId = id; + const agentName = normalizeAgent(input.agent); + if (!isTrellisAgent(root, agentName)) { + return { + content: [ + { + type: "text", + text: + "`trellis_subagent` is only for Trellis workflow agents with a definition file in .pi/agents/.\n\n" + + `No definition found for: ${agentName}\n\n` + + "For general-purpose sub-agents, use one of these community tools:\n" + + "- `subagent` tool from npm:pi-subagents (nicobailon/pi-subagents)\n" + + "- `Agent` tool from npm:@tintinweb/pi-subagents\n\n" + + "If neither is installed, ask the user to either:\n" + + `- Create .pi/agents/${agentName}.md for your custom Trellis agent\n` + + "- Install a community subagent package: pi install -l npm:@tintinweb/pi-subagents", + }, + ], + details: { agent: agentName, error: "not a trellis workflow agent" }, + }; + } + const mode = input.mode ?? "single"; + const prompt = input.prompt?.trim(); + const prompts = input.prompts?.map((p) => p.trim()).filter(Boolean); + if (mode === "single" && !prompt) + throw new Error("subagent prompt is required for single mode"); + if ( + (mode === "parallel" || mode === "chain") && + !prompt && + !prompts?.length + ) + throw new Error( + "subagent prompt or prompts are required for parallel/chain mode", + ); + if ( + mode === "parallel" && + prompts && + prompts.length > MAX_PARALLEL_PROMPTS + ) + throw new Error( + `subagent parallel mode supports at most ${MAX_PARALLEL_PROMPTS} prompts`, + ); + const cleanInput: SubagentInput = { + ...input, + prompt, + prompts: prompts?.length ? prompts : undefined, + }; + const key = getKey(cleanInput, ctx); + const inheritedThinking = pi.getThinkingLevel?.(); + const result = await runSubagent( + root, + cleanInput, + key, + signal, + onUpdate, + inheritedThinking, + ); + return { + content: [{ type: "text", text: result.output }], + details: result.details, + }; + }, + // Hide the call renderer so the native card only shows result/progress once. + renderCall: () => ({ + render() { + return []; + }, + invalidate() {}, + }), + renderResult: ( + result: PiToolResult, + _opts?: { expanded?: boolean; isPartial?: boolean }, + _theme?: unknown, + context?: unknown, + ) => { + const ctxObj = isObj(context) ? context : null; + const toolCallId = str(ctxObj?.toolCallId); + const state = isObj(ctxObj?.state) ? (ctxObj.state as JsonObject) : null; + const invalidate = + typeof ctxObj?.invalidate === "function" + ? (ctxObj.invalidate as () => void) + : null; + const isProgress = + isObj(result.details) && + result.details.kind === "trellis-subagent-progress"; + if (toolCallId && state && invalidate) { + const updatedAt = isProgress + ? (result.details as ProgressDetails).updatedAt + : Date.now(); + rememberNativeCard(toolCallId, { state, invalidate, updatedAt }); + } + return { + render(w: number) { + if (isProgress) { + const expanded = state?.localExpanded === true; + return renderProgressCard( + result.details as ProgressDetails, + expanded, + w, + ); + } + return [trunc(result.content?.[0]?.text ?? "(no output)", w)]; + }, + invalidate() {}, + }; + }, + }); + + // Events + pi.on?.("session_start", (event, ctx) => { + getKey(event, ctx); + ctx?.ui?.notify?.( + "Trellis project context is available. Use /trellis-start to bootstrap or /trellis-continue to resume.", + "info", + ); + }); + pi.on?.("session_shutdown", () => { + nativeCards.clear(); + activeSubagentToolCallId = null; + }); + pi.on?.("tool_call", (event, ctx) => { + const k = getKey(event, ctx); + const ev = event as { toolName?: string; input?: JsonObject }; + if ( + ev.toolName === "bash" && + isObj(ev.input) && + typeof ev.input.command === "string" && + !cmdHasTrellisCtx(ev.input.command) + ) + ev.input.command = `export TRELLIS_CONTEXT_ID=${shellQuote(k)}; ${ev.input.command}`; + }); + // Preserve progress details from execute(); mark failed subagent results through + // the official tool_result patch hook instead of throwing away renderer details. + pi.on?.("tool_result", (event) => { + const ev = event as { toolName?: string; details?: unknown }; + if ( + ev.toolName === "trellis_subagent" && + isObj(ev.details) && + ev.details.kind === "trellis-subagent-progress" && + Array.isArray(ev.details.runs) && + ev.details.runs.some( + (r) => isObj(r) && (r.status === "failed" || r.status === "cancelled"), + ) + ) + return { isError: true }; + return undefined; + }); + pi.on?.("input", (event, ctx) => { + const k = getKey(event, ctx); + const ev = event as { text?: string }; + if (typeof ev.text !== "string" || !ev.text.trim()) + return { action: "continue" }; + const { wf, ov } = getTurnCtx(k); + const injection = [wf, ov].filter(Boolean).join("\n\n"); + if (!injection) return { action: "continue" }; + return { + action: "transform", + text: [ev.text, injection].join("\n\n"), + }; + }); + pi.on?.("before_agent_start", (event, ctx) => { + const k = getKey(event, ctx); + const cur = (event as { systemPrompt?: string }).systemPrompt ?? ""; + const ctxText = buildContext(root, "trellis-implement", k); + const turn = getTurnCtx(k); + const startup = getStartupCtx(k, turn); + return { + systemPrompt: [cur, startup, ctxText, turn.wf, turn.ov] + .filter(Boolean) + .join("\n\n"), + }; + }); + pi.on?.("context", (event, ctx) => { + getKey(event, ctx); + }); +} diff --git a/.pi/prompts/trellis-continue.md b/.pi/prompts/trellis-continue.md new file mode 100644 index 0000000..5615364 --- /dev/null +++ b/.pi/prompts/trellis-continue.md @@ -0,0 +1,56 @@ +# Continue Current Task + +Resume work on the current task — pick up at the right phase/step in `.trellis/workflow.md`. + +--- + +## Step 1: Load Current Context + +```bash +python3 ./.trellis/scripts/get_context.py +``` + +Confirms: current task, git state, recent commits. + +## Step 2: Load the Phase Index + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase +``` + +Shows the Phase Index (Plan / Execute / Finish) with routing + skill mapping. + +## Step 3: Decide Where You Are + +`get_context.py` shows the active task's `status` field. Route by `status` + artifact presence. 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` 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.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 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 + +Once you know which step to resume at: + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase --step <X.X> --platform pi +``` + +Follow the loaded instructions. After each `[required]` step completes, move to the next. + +--- + +## Reference + +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/.pi/prompts/trellis-finish-work.md b/.pi/prompts/trellis-finish-work.md new file mode 100644 index 0000000..54c2f67 --- /dev/null +++ b/.pi/prompts/trellis-finish-work.md @@ -0,0 +1,66 @@ +# Finish Work + +Wrap up the current session: archive the active task (and any other completed-but-unarchived tasks the user wants to clean up) and record the session journal. Code commits are NOT done here — those happen in workflow Phase 3.4 before you invoke this command. + +## Step 1: Survey current state + +```bash +python3 ./.trellis/scripts/get_context.py --mode record +``` + +This prints: + +- **My active tasks** — review whether any besides the current one are actually done (code merged, AC met) and should be archived this round. +- **Git status** — quick visual on what's dirty. +- **Recent commits** — you'll need their hashes in Step 4 for `--commit`. + +If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless. + +## Step 2: Sanity check — classify dirty paths + +Run: + +```bash +git status --porcelain +``` + +Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this skill's own work. + +For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics: + +- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task +- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task +- Paths in unrelated areas you have no recollection of touching this session → other parallel work + +Then route: + +- **Any remaining path looks like current-task work** — bail out with: + > "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running `/trellis-finish-work`." + + Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there. +- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3: + > "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`." +- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer. + +## Step 3: Archive task(s) + +```bash +python3 ./.trellis/scripts/task.py archive <task-name> +``` + +At minimum: the current active task (if any). Plus any extra tasks the user confirmed in Step 1. Each archive produces a `chore(task): archive ...` commit via the script's auto-commit. + +If there is no active task and the user did not confirm any cleanup archives, skip this step. + +## Step 4: Record session journal + +```bash +python3 ./.trellis/scripts/add_session.py \ + --title "Session Title" \ + --commit "hash1,hash2" \ + --summary "Brief summary" +``` + +Use the work-commit hashes produced in Phase 3.4 (visible in Step 1's `Recent commits` list, or via `git log --oneline`) for `--commit`. Do not include the archive commit hashes from Step 3. This produces a `chore: record journal` commit. + +Final git log order: `<work commits from 3.4>` → `chore(task): archive ...` (one or more) → `chore: record journal`. diff --git a/.pi/prompts/trellis-start.md b/.pi/prompts/trellis-start.md new file mode 100644 index 0000000..3bfa678 --- /dev/null +++ b/.pi/prompts/trellis-start.md @@ -0,0 +1,59 @@ +# Start Session + +Initialize a Trellis-managed development session. This platform has no session-start hook, so manually load the equivalent compact context by following these steps. + +--- + +## Step 1: Current state +Identity, git status, current task, active tasks, journal location. + +```bash +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 +Compact Phase Index, request triage rules, planning artifact contract, and the step-detail command. + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase +``` + +Full guide in `.trellis/workflow.md` (read on demand). + +## Step 3: Guideline indexes +Discover packages + spec layers, then read each relevant index file. + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +cat .trellis/spec/guides/index.md +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 and status. Check the task directory: + +- **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 pi + ``` +- **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. + +--- + +## Skill routing (quick reference) + +| User intent | Skill | +|---|---| +| New feature / unclear requirements | `trellis-brainstorm` | +| About to write code | `trellis-before-dev` | +| Done coding / quality check | `trellis-check` | +| Stuck / fixed same bug multiple times | `trellis-break-loop` | +| Learned something worth capturing | `trellis-update-spec` | + +Full rules + anti-rationalization table in `.trellis/workflow.md`. diff --git a/.pi/settings.json b/.pi/settings.json new file mode 100644 index 0000000..5f3acce --- /dev/null +++ b/.pi/settings.json @@ -0,0 +1,12 @@ +{ + "enableSkillCommands": true, + "extensions": [ + "./extensions/trellis/index.ts" + ], + "skills": [ + "./skills" + ], + "prompts": [ + "./prompts" + ] +} diff --git a/.pi/skills/trellis-before-dev/SKILL.md b/.pi/skills/trellis-before-dev/SKILL.md new file mode 100644 index 0000000..5a4b852 --- /dev/null +++ b/.pi/skills/trellis-before-dev/SKILL.md @@ -0,0 +1,40 @@ +--- +name: trellis-before-dev +description: "Discovers and injects project-specific coding guidelines from .trellis/spec/ before implementation begins. Reads spec indexes, pre-development checklists, and shared thinking guides for the target package. Use when starting a new coding task, before writing any code, switching to a different package, or needing to refresh project conventions and standards." +--- + +Read the relevant development guidelines before starting your task. + +Execute these steps: + +1. **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 + ``` + +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 + +4. **Read the spec index** for each relevant module: + ```bash + cat .trellis/spec/<package>/<layer>/index.md + ``` + Follow the **"Pre-Development Checklist"** section in the index. + +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. + +6. **Always read shared guides**: + ```bash + cat .trellis/spec/guides/index.md + ``` + +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/.pi/skills/trellis-brainstorm/SKILL.md b/.pi/skills/trellis-brainstorm/SKILL.md new file mode 100644 index 0000000..806ee31 --- /dev/null +++ b/.pi/skills/trellis-brainstorm/SKILL.md @@ -0,0 +1,173 @@ +--- +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." +--- + +# Trellis Brainstorm + +## Non-Negotiable Interview Contract + +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. + +Ask the questions one at a time. + +## 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. + +Do not ask the user to confirm facts that the repository can answer. Ask only for product intent, preference, scope, risk tolerance, or decisions that remain ambiguous after inspection. + +--- + +Use this skill during Phase 1 planning to turn the user's request into clear requirements and planning artifacts. + +## Preconditions + +Use this skill only after task-creation consent has been given and the user is ready to enter Trellis planning. + +If no task exists yet, create one: + +```bash +TASK_DIR=$(python3 ./.trellis/scripts/task.py create "<short task title>" --slug <slug>) +``` + +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. + +`task.py create` creates the default `prd.md`. Update that file with the current understanding before asking follow-up questions. + +## Planning Flow + +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. Ask the single highest-value remaining question. +5. Include your recommended answer with the question. +6. After each user answer, update `prd.md` before continuing. +7. For complex tasks, create or update `design.md` and `implement.md` before implementation starts. +8. Before final review or `task.py start`, run the PRD convergence pass below. + +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. + +## Question Rules + +Ask only one question per message. + +Each question must include: + +- the decision needed +- why the answer matters +- your recommended answer +- the trade-off if the user chooses differently + +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. + +## 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. + +### Step 1: Restate the Problem + +Strip away implementation details to one sentence. + +> 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 + +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 + +For each component of the current plan: + +- **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. + +### Step 4: Build Up from Truths + +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?" + +### Step 5: Validate + +- Does the solution solve the original problem? +- What assumptions need verification? +- What's the simplest experiment to test this? + +## Artifact Rules + +`prd.md` records requirements and acceptance: + +- goal and user value +- confirmed facts +- requirements +- acceptance criteria +- out of scope +- open questions that still block planning + +`design.md` records technical design for complex tasks: + +- architecture and boundaries +- data flow and contracts +- compatibility and migration notes +- important trade-offs +- operational or rollback considerations + +`implement.md` records execution planning for complex tasks: + +- ordered implementation checklist +- validation commands +- risky files or rollback points +- follow-up checks before `task.py start` + +Lightweight tasks may have only `prd.md`. Complex tasks must have `prd.md`, `design.md`, and `implement.md` before `task.py start`. + +`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`. + +## PRD Convergence Pass + +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. + +The pass must be lossless: + +- 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. +- Keep only genuinely blocking open questions. + +After the pass, read `prd.md` top to bottom and verify that no fact is repeated across sections unless the repetition adds new information. + +## Quality Bar + +Before declaring planning ready: + +- `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. +- Remaining open questions are genuinely about user intent or scope. +- 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 user has reviewed the final planning artifacts or explicitly approved proceeding. + +Do not start implementation until the user approves or asks for implementation. diff --git a/.pi/skills/trellis-break-loop/SKILL.md b/.pi/skills/trellis-break-loop/SKILL.md new file mode 100644 index 0000000..1c8b397 --- /dev/null +++ b/.pi/skills/trellis-break-loop/SKILL.md @@ -0,0 +1,188 @@ +--- +name: trellis-break-loop +description: "Deep bug analysis to break the fix-forget-repeat cycle. Analyzes root cause category, why fixes failed, prevention mechanisms, and captures knowledge into specs. Use after fixing a bug to prevent the same class of bugs." +--- + +# Break the Loop - Deep Bug Analysis + +When debug is complete, use this for deep analysis to break the "fix bug -> forget -> repeat" cycle. + +--- + +## Analysis Framework + +Analyze the bug you just fixed from these 5 dimensions: + +### 1. Root Cause Category + +Which category does this bug belong to? + +| Category | Characteristics | Example | +|----------|-----------------|---------| +| **A. Missing Spec** | No documentation on how to do it | New feature without checklist | +| **B. Cross-Layer Contract** | Interface between layers unclear | API returns different format than expected | +| **C. Change Propagation Failure** | Changed one place, missed others | Changed function signature, missed call sites | +| **D. Test Coverage Gap** | Unit test passes, integration fails | Works alone, breaks when combined | +| **E. Implicit Assumption** | Code relies on undocumented assumption | Timestamp seconds vs milliseconds | + +### 2. Why Fixes Failed (if applicable) + +If you tried multiple fixes before succeeding, analyze each failure: + +- **Surface Fix**: Fixed symptom, not root cause +- **Incomplete Scope**: Found root cause, didn't cover all cases +- **Tool Limitation**: grep missed it, type check wasn't strict +- **Mental Model**: Kept looking in same layer, didn't think cross-layer + +### 3. Prevention Mechanisms + +What mechanisms would prevent this from happening again? + +| Type | Description | Example | +|------|-------------|---------| +| **Documentation** | Write it down so people know | Update thinking guide | +| **Architecture** | Make the error impossible structurally | Type-safe wrappers | +| **Compile-time** | Strict type checking, no escape hatches | Signature change causes compile error | +| **Runtime** | Monitoring, alerts, scans | Detect orphan entities | +| **Test Coverage** | E2E tests, integration tests | Verify full flow | +| **Code Review** | Checklist, PR template | "Did you check X?" | + +### 4. Systematic Expansion + +What broader problems does this bug reveal? + +- **Similar Issues**: Where else might this problem exist? +- **Design Flaw**: Is there a fundamental architecture issue? +- **Process Flaw**: Is there a development process improvement? +- **Knowledge Gap**: Is the team missing some understanding? + +### 5. Knowledge Capture + +Solidify insights into the system: + +- [ ] Update `.trellis/spec/guides/` thinking guides +- [ ] Update relevant `.trellis/spec/` docs +- [ ] Create issue record (if applicable) +- [ ] Create feature ticket for root fix +- [ ] Update check guidelines if needed + +--- + +## Output Format + +Please output analysis in this format: + +```markdown +## Bug Analysis: [Short Description] + +### 1. Root Cause Category +- **Category**: [A/B/C/D/E] - [Category Name] +- **Specific Cause**: [Detailed description] + +### 2. Why Fixes Failed (if applicable) +1. [First attempt]: [Why it failed] +2. [Second attempt]: [Why it failed] +... + +### 3. Prevention Mechanisms +| Priority | Mechanism | Specific Action | Status | +|----------|-----------|-----------------|--------| +| P0 | ... | ... | TODO/DONE | + +### 4. Systematic Expansion +- **Similar Issues**: [List places with similar problems] +- **Design Improvement**: [Architecture-level suggestions] +- **Process Improvement**: [Development process suggestions] + +### 5. Knowledge Capture +- [ ] [Documents to update / tickets to create] +``` + +--- + +## Core Philosophy + +> **The value of debugging is not in fixing the bug, but in making this class of bugs never happen again.** + +Three levels of insight: +1. **Tactical**: How to fix THIS bug +2. **Strategic**: How to prevent THIS CLASS of bugs +3. **Philosophical**: How to expand thinking patterns + +30 minutes of analysis saves 30 hours of future debugging. + +## 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 + +**IMPORTANT**: After completing the analysis above, you MUST immediately: + +1. **Update spec/guides** - Don't just list TODOs, actually update the relevant files: + - If it's a cross-platform issue → update `cross-platform-thinking-guide.md` + - If it's a cross-layer issue → update `cross-layer-thinking-guide.md` + - If it's a code reuse issue → update `code-reuse-thinking-guide.md` + - If it's domain-specific → update `backend/*.md` or `frontend/*.md` + +2. **Sync templates** - After updating `.trellis/spec/`, sync to `src/templates/markdown/spec/` + +3. **Commit the spec updates** - This is the primary output, not just the analysis text + +> **The analysis is worthless if it stays in chat. The value is in the updated specs.** diff --git a/.pi/skills/trellis-channel/SKILL.md b/.pi/skills/trellis-channel/SKILL.md new file mode 100644 index 0000000..511ee02 --- /dev/null +++ b/.pi/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 <board> --scope global +trellis channel thread <board> <thread> --scope global +trellis channel context list <board> --scope global --thread <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 <my_signal>` 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/.pi/skills/trellis-channel/references/command-reference.md b/.pi/skills/trellis-channel/references/command-reference.md new file mode 100644 index 0000000..75def26 --- /dev/null +++ b/.pi/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 <project|global>` unless noted; `project` +is the default and resolves against the current cwd's project bucket. + +## Top-level + +``` +trellis channel <subcommand> +``` + +> Multi-agent collaboration runtime — spawn / coordinate / interrupt worker +> agents through a shared event log. + +--- + +## Create / List + +### `create <name>` + +```bash +trellis channel create <name> + [--scope project|global] # default: project + [--type chat|forum] # default: chat + [--task <path>] # associated Trellis task dir + [--project <slug>] + [--labels a,b,c] + [--description <text>] # stable channel description + [--context-file <abs-path>] ... # repeatable + [--context-raw <text>] ... # repeatable + [--linked-context-file <abs-path>] # [deprecated alias] + [--linked-context-raw <text>] # [deprecated alias] + [--cwd <path>] # recorded in create event + [--by <agent>] # 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 <slug>] # 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 <name> [text]` + +```bash +trellis channel send <name> [text] + --as <agent> # REQUIRED — author + [--scope project|global] + [--to <agents,csv>] # default: broadcast + [--stdin | --text-file <path>] # 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 <name>` + +```bash +trellis channel messages <name> + [--scope project|global] + [--raw] # one JSON event per line + [--follow] # stream new events + [--last <N>] # last N matching events + [--since <seq>] # seq > N + [--kind <kind>] # one of CHANNEL_EVENT_KINDS + [--from <csv>] # author filter + [--to <target>] # routing target filter + [--thread <key>] # forum-only + [--action <thread-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 <name>` + +```bash +trellis channel wait <name> + --as <agent> # REQUIRED — self for filter ctx + [--scope project|global] + [--timeout <Ns|Nm|Nh|Nms>] # parsed by parseDuration + [--from <a,b>] # author CSV + [--kind <k1,k2>] # CSV, OR semantics + [--thread <key>] # forum filter + [--action <thread-action>] # forum filter + [--to <target>] # 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 '<x>'. 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 <name> [text]` + +```bash +trellis channel interrupt <name> [text] + --as <agent> # REQUIRED — caller + --to <agent> # REQUIRED — target worker + [--scope project|global] + [--stdin | --text-file <path>] +``` + +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 <name>` + +```bash +trellis channel spawn <name> + [--scope project|global] + [--agent <agent-name>] # loads .trellis/agents/<name>.md + [--provider claude|codex] # overrides agent file + [--as <worker-name>] # default: agent name + [--cwd <path>] + [--model <id>] + [--resume <id>] # session/thread id resume + [--timeout <Ns|Nm|Nh>] # auto-kill after duration + [--warn-before <Ns|Nm|Nh>] # supervisor_warning lead time + # default 5m, 0ms disables + [--file <path>] ... # glob, repeatable; inject content + [--jsonl <path>] ... # Trellis manifest, repeatable + [--by <agent>] # spawn-event author + # default: TRELLIS_CHANNEL_AS env or 'main' + [--inbox-policy explicitOnly|broadcastAndExplicit] + # default explicitOnly + [--idle-timeout <Ns|Nm|Nh>] # OOM-guard idle TTL + # default 5m, 0 disables + [--max-live-workers <n>] # 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 <worker>`. +- 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 <name>] + [--provider claude|codex] + [--as <worker-name>] + [--cwd <path>] + [--model <id>] + [--file <path>] ... # repeatable, glob + [--jsonl <path>] ... # repeatable + [--message <text> | --message-file <path> | --stdin] + [--timeout <Ns|Nm|Nh>] # default 5m +``` + +Behavior: +- One-shot. Auto-generates `run-<hex>` 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 <name>` + +```bash +trellis channel kill <name> + --as <agent> # 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 <name>` + +```bash +trellis channel rm <name> + [--scope project|global] +``` + +Behavior: +- Kills any live workers, then deletes the entire channel directory. +- Prints `Removed channel '<name>'`. + +### `prune` + +```bash +trellis channel prune + [--scope project|global] # omitted: scan every project + [--all | --empty | --idle <Ns|Nm|Nh|Nd> | --ephemeral] # mutually exclusive + [--yes] # actually delete (default: dry-run) + [--dry-run] # default true; redundant with default + [--keep <names,csv>] # 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 <name> <action>` + +```bash +trellis channel post <name> <action> + --as <agent> # REQUIRED + [--scope project|global] + [--thread <key>] # required except action=opened + [--title <text>] + [--text <text> | --stdin | --text-file <path>] + [--description <text>] # stable thread description + [--status <status>] + [--labels a,b] # REPLACES thread labels + [--assignees a,b] # REPLACES assignees + [--summary <text>] + [--context-file <abs-path>] ... + [--context-raw <text>] ... + [--linked-context-file <abs-path>] # [deprecated alias] + [--linked-context-raw <text>] # [deprecated alias] +``` + +Behavior: +- `<action>` 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 <name>` + +```bash +trellis channel forum <name> + [--scope project|global] + [--status <status>] + [--raw] +``` + +Behavior: +- Lists threads (reduced state). `--status` filters by current thread + status. `--raw` prints one JSON per thread. + +### `thread <name> <thread>` / `thread rename` + +```bash +trellis channel thread <name> <thread-key> + [--scope project|global] + [--raw] + +trellis channel thread rename <name> <old-thread> <new-thread> + --as <agent> # REQUIRED + [--scope project|global] +``` + +Behavior: +- `thread <name> <key>` shows one thread's timeline: + header `<thread> [<status>] <title>`, 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/.pi/skills/trellis-channel/references/forum.md b/.pi/skills/trellis-channel/references/forum.md new file mode 100644 index 0000000..06b7f36 --- /dev/null +++ b/.pi/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/.pi/skills/trellis-channel/references/progress-debugging.md b/.pi/skills/trellis-channel/references/progress-debugging.md new file mode 100644 index 0000000..3ed40d6 --- /dev/null +++ b/.pi/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/.pi/skills/trellis-channel/references/workers.md b/.pi/skills/trellis-channel/references/workers.md new file mode 100644 index 0000000..bcec98f --- /dev/null +++ b/.pi/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/.pi/skills/trellis-channel/references/workflows.md b/.pi/skills/trellis-channel/references/workflows.md new file mode 100644 index 0000000..3319764 --- /dev/null +++ b/.pi/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/.pi/skills/trellis-check/SKILL.md b/.pi/skills/trellis-check/SKILL.md new file mode 100644 index 0000000..c695abd --- /dev/null +++ b/.pi/skills/trellis-check/SKILL.md @@ -0,0 +1,98 @@ +--- +name: trellis-check +description: "Comprehensive quality verification: spec compliance, lint, type-check, tests, cross-layer data flow, code reuse, and consistency checks. Use when code is written and needs quality verification, before committing changes, or to catch context drift during long sessions." +--- + +# Code Quality Check + +Comprehensive quality verification for recently written code. Combines spec compliance, cross-layer safety, and pre-commit checks. + +--- + +## Step 1: Identify What Changed + +```bash +git diff --name-only HEAD +git status +``` + +## Step 2: Read 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 +``` + +For each changed package/layer, read the spec index and follow its **Quality Check** section: + +```bash +cat .trellis/spec/<package>/<layer>/index.md +``` + +Read the specific guideline files referenced — the index is a pointer, not the goal. + +## Step 3: Run Project Checks + +Run the project's lint, type-check, and test commands. Fix any failures before proceeding. + +## Step 4: Review Against Checklist + +### Code Quality + +- [ ] Linter passes? +- [ ] Type checker passes (if applicable)? +- [ ] Tests pass? +- [ ] No debug logging left in? +- [ ] No suppressed warnings or type-safety bypasses? + +### Test Coverage + +- [ ] New function → unit test added? +- [ ] Bug fix → regression test added? +- [ ] Changed behavior → existing tests updated? + +### Spec Sync + +- [ ] Does `.trellis/spec/` need updates? (new patterns, conventions, lessons learned) + +> "If I fixed a bug or discovered something non-obvious, should I document it so future me won't hit the same issue?" → If YES, update the relevant spec doc. + +## Step 5: Cross-Layer Dimensions (if applicable) + +Skip this step if your change is confined to a single layer. + +### A. Data Flow (changes touch 3+ layers) + +- [ ] Read flow traces correctly: Storage → Service → API → UI +- [ ] Write flow traces correctly: UI → API → Service → Storage +- [ ] Types/schemas correctly passed between layers? +- [ ] Errors properly propagated to caller? + +### B. Code Reuse (modifying constants, creating utilities) + +- [ ] Searched for existing similar code before creating new? + ```bash + grep -r "pattern" src/ + ``` +- [ ] If 2+ places define same value → extracted to shared constant? +- [ ] After batch modification, all occurrences updated? + +### C. Import/Dependency (creating new files) + +- [ ] Correct import paths (relative vs absolute)? +- [ ] No circular dependencies? + +### D. Same-Layer Consistency + +- [ ] Other places using the same concept are consistent? + +--- + +## Step 6: Report and Fix + +Report violations found and fix them directly. Re-run project checks after fixes. diff --git a/.pi/skills/trellis-meta/SKILL.md b/.pi/skills/trellis-meta/SKILL.md new file mode 100644 index 0000000..0754b53 --- /dev/null +++ b/.pi/skills/trellis-meta/SKILL.md @@ -0,0 +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, 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, bundled runtime agents, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.reasonix/`, `.kilocode/`, `.agent/`, `.devin/`, 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. +- 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` — 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 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 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 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 15 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, 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; 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` 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/.pi/skills/trellis-meta/references/customize-local/add-project-local-conventions.md b/.pi/skills/trellis-meta/references/customize-local/add-project-local-conventions.md new file mode 100644 index 0000000..608aaa6 --- /dev/null +++ b/.pi/skills/trellis-meta/references/customize-local/add-project-local-conventions.md @@ -0,0 +1,83 @@ +# Add Project-Local Conventions + +Often the user does not need to change Trellis mechanics; they need local AI to understand their team's conventions. In that case, prefer `.trellis/spec/` or a project-local skill instead of editing `trellis-meta`. + +## Where To Put Things + +| Content type | Location | +| --- | --- | +| Rules code must follow | `.trellis/spec/<layer>/` | +| Cross-layer thinking methods | `.trellis/spec/guides/` | +| AI capability for a project-specific flow | Platform-local skill | +| One-off task material | `.trellis/tasks/<task>/` | +| Session summary | `.trellis/workspace/<developer>/journal-N.md` | + +## Create A Project-Local Skill + +If the user wants AI to know "how this project customizes Trellis," create a local skill: + +```text +.claude/skills/trellis-local/ +└── SKILL.md +``` + +Example: + +```md +--- +name: trellis-local +description: "Project-local Trellis customizations for this repository. Use when changing this project's Trellis workflow, hooks, local agents, or team-specific conventions." +--- + +# Trellis Local + +## Local Scope + +This skill documents this repository's Trellis customizations only. + +## Custom Workflow Rules + +- ... + +## Local Hook Changes + +- ... + +## Local Agent Changes + +- ... +``` + +For multi-platform projects, place equivalent versions in other platform skill directories, or use `.agents/skills/` for platforms that support the shared layer. + +## Write To `.trellis/spec/` + +If the content is a coding convention, write it to spec. Examples: + +```text +.trellis/spec/backend/error-handling.md +.trellis/spec/frontend/components.md +.trellis/spec/guides/cross-platform-thinking-guide.md +``` + +After writing it, update the corresponding `index.md` so AI can find the new rule from the entry point. + +## Make The Current Task Use New Conventions + +After writing a spec, add it to the current task context: + +```bash +python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/backend/error-handling.md" "Error handling conventions" +python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/backend/error-handling.md" "Review error handling" +``` + +## Do Not Store Project-Private Rules In `trellis-meta` + +`trellis-meta` is a public skill for understanding Trellis architecture and local customization entry points. Put project-private content in: + +- `.trellis/spec/` +- a project-local skill +- the current task +- workspace journal + +This prevents future updates to Trellis's built-in `trellis-meta` from overwriting the team's own conventions. diff --git a/.pi/skills/trellis-meta/references/customize-local/change-agents.md b/.pi/skills/trellis-meta/references/customize-local/change-agents.md new file mode 100644 index 0000000..88e7376 --- /dev/null +++ b/.pi/skills/trellis-meta/references/customize-local/change-agents.md @@ -0,0 +1,56 @@ +# Change Local Agents + +When the user wants to change `trellis-research`, `trellis-implement`, or `trellis-check` behavior, edit platform agent files in the user project. + +## Read These Files First + +1. Target platform agent directory +2. `.trellis/workflow.md` Phase 2 / research routing +3. Current task `prd.md` +4. Current task `implement.jsonl` / `check.jsonl` +5. Relevant hook or agent prelude + +## Common Paths + +| Platform | Path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/cli/agents/trellis-*.md` | + +Use the actual paths in the user project as authoritative. + +## Common Needs + +| Need | Which agent to edit | +| --- | --- | +| Research must write files, not only reply in chat | `trellis-research` | +| Certain local specs must be read before implementation | `trellis-implement` + `implement.jsonl` configuration rules | +| Specific commands must run during checking | `trellis-check` | +| Agent must not modify certain directories | The corresponding agent's write boundary instructions | +| Agent output format must be fixed | The corresponding agent's final/reporting instructions | + +## Modification Principles + +1. **Preserve role boundaries**: research investigates and persists; implement writes implementation; check reviews and fixes. +2. **Do not hard-code project specs into agents**: long-term specs belong in `.trellis/spec/`; agents are responsible for reading them. +3. **Make read order explicit**: active task -> PRD -> info -> JSONL -> spec/research. +4. **Make write boundaries explicit**: which directories may be written and which may not. +5. **Synchronize across platforms**: when the user configured multiple platforms, decide whether to change only the current platform or all platform agents. + +## Agent Pull Platforms + +If an agent file contains a prelude for "read task/context after startup," do not remove those steps when editing. Otherwise the agent will work only from chat context and bypass Trellis's core mechanism. + +## Hook Push Platforms + +If context is injected by a hook, the agent file should still retain responsibility boundaries. Do not remove PRD/spec requirements from the agent just because a hook injects context. diff --git a/.pi/skills/trellis-meta/references/customize-local/change-context-loading.md b/.pi/skills/trellis-meta/references/customize-local/change-context-loading.md new file mode 100644 index 0000000..002a259 --- /dev/null +++ b/.pi/skills/trellis-meta/references/customize-local/change-context-loading.md @@ -0,0 +1,84 @@ +# Change Local Context Loading + +Context loading determines when AI reads workflow, task, spec, research, workspace, and git status. Read this page when the user says "AI does not know the current task," "the agent did not read specs," or "there is too much/too little context." + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/scripts/get_context.py` +3. `.trellis/scripts/common/session_context.py` +4. `.trellis/scripts/common/task_context.py` +5. `.trellis/scripts/common/active_task.py` +6. Current platform hooks or agent files +7. The current task's `implement.jsonl` / `check.jsonl` + +## Context Sources + +| Source | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow and next-action hints. | +| `.trellis/tasks/<task>/prd.md` | Current task requirements. | +| `.trellis/tasks/<task>/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. | +| `.trellis/workspace/` | Session records. | +| git status | Current working tree changes. | + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Inject more/less information in new sessions | `session_context.py` or the platform `session-start` hook. | +| Change hints on each user input | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The `inject-workflow-state` hook is parser-only and reads the block verbatim. | +| Agent did not read specs | Task JSONL, agent prelude, `inject-subagent-context` hook. | +| Active task is lost | `active_task.py` and platform session identity propagation. | +| Change JSONL validation rules | `task_context.py`. | + +## JSONL Rules + +`implement.jsonl` / `check.jsonl` are the key context loading interface: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-x/research/api.md", "reason": "API research"} +``` + +Include only spec/research files. Do not put code files that will be modified into these manifests; agents read code files themselves during implementation. + +## Change Session Context + +If the user wants every new session to see more project state, edit: + +- `.trellis/scripts/common/session_context.py` +- the corresponding platform `session-start` hook + +Context cannot grow without bound. Prefer injecting indexes and paths so the AI can read detailed files on demand. + +## Change Sub-Agent Context + +First determine which mode the platform uses: + +- hook push: edit the `inject-subagent-context` hook. +- agent pull: edit the read steps in the corresponding `trellis-implement` / `trellis-check` agent file. + +In both modes, make sure the agent ultimately reads: + +1. active task +2. 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 + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py list-context <task> +python3 ./.trellis/scripts/task.py validate <task> +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Confirm the task and JSONL are correct before editing hooks/agents. diff --git a/.pi/skills/trellis-meta/references/customize-local/change-hooks.md b/.pi/skills/trellis-meta/references/customize-local/change-hooks.md new file mode 100644 index 0000000..79aa5c5 --- /dev/null +++ b/.pi/skills/trellis-meta/references/customize-local/change-hooks.md @@ -0,0 +1,57 @@ +# Change Local Hooks + +Hooks are the automation layer that connects a platform to Trellis. When the user wants to change "when context is injected," "how shell commands inherit a session," or "which files are read before an agent starts," hooks are usually the edit point. + +## Read These Files First + +1. Target platform settings/config, such as `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`, `.trae/hooks.json` +2. Target platform hooks directory +3. `.trellis/scripts/common/active_task.py` +4. `.trellis/scripts/common/session_context.py` +5. `.trellis/workflow.md` + +## Common Hook Types + +| Hook | Purpose | +| --- | --- | +| session-start | Injects a Trellis overview when a session starts, clears, or compacts. | +| workflow-state | Injects a state hint on each user input. | +| sub-agent context | Injects PRD/spec/research before an agent starts. | +| shell session bridge | Lets `task.py` commands in shell see the same session identity. | + +## Modification Steps + +1. Find the hook registration in settings/config. +2. Confirm the registered script path exists. +3. Read the hook script and identify inputs, outputs, and called `.trellis/scripts/`. +4. Modify hook behavior. +5. If the hook depends on workflow content, synchronize `.trellis/workflow.md`. + +## Example: Change New-Session Injection Content + +First find the session-start hook: + +```text +.claude/settings.json +.claude/hooks/session-start.py +``` + +If the hook ultimately calls `.trellis/scripts/get_context.py` or `session_context.py`, editing the local script is usually more robust than hard-coding content in the hook. + +## Example: Agent Did Not Read JSONL + +First confirm: + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py validate <task> +``` + +If the task and JSONL are correct, determine whether the platform uses hook push or agent pull. For hook push, edit `inject-subagent-context`; for agent pull, edit the agent file. + +## Notes + +- Settings handle registration, hook scripts handle behavior; inspect both together. +- Different platforms support different hook events. Do not directly copy another platform's settings. +- Hooks should read project-local `.trellis/`; they should not depend on Trellis upstream source paths. +- Hook failures should produce visible errors so AI does not silently lose context. diff --git a/.pi/skills/trellis-meta/references/customize-local/change-skills-or-commands.md b/.pi/skills/trellis-meta/references/customize-local/change-skills-or-commands.md new file mode 100644 index 0000000..0ed8792 --- /dev/null +++ b/.pi/skills/trellis-meta/references/customize-local/change-skills-or-commands.md @@ -0,0 +1,123 @@ +# Change Local Skills, Commands, Prompts, And Workflows + +When the user wants to change AI entry points, auto-trigger rules, or explicit command behavior, edit skills, commands, prompts, or workflows in local platform directories. + +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 + +| Goal | Recommendation | +| --- | --- | +| AI should automatically know a capability | Add or modify a skill. | +| User wants to trigger manually with a command | Add or modify a command/prompt/workflow. | +| Team project conventions | Prefer `.trellis/spec/` or a project-local skill — 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 + +A skill is usually: + +```text +<skill-name>/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should be short and responsible for triggering/routing. Put long content in `references/` so AI can read it on demand. + +The frontmatter description should specify when to use the skill. Example: + +```yaml +description: "Use when customizing this project's deployment workflow and release checklist." +``` + +Do not write vague descriptions such as "helpful project skill"; they can trigger incorrectly. + +### 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: + +- How the user triggers it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +If a command only repeats workflow rules, prefer making it reference/read `.trellis/workflow.md` instead of maintaining a second copy of the flow. + +## Common Paths + +| Platform | Entry directories | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| 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/` | +| Factory Droid | `.factory/skills/`, `.factory/commands/` | +| Pi Agent | `.pi/skills/` | +| Reasonix | `.reasonix/skills/` (no separate commands dir; slash commands built into the platform) | +| ZCode | `.agents/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 — 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 (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/.pi/skills/trellis-meta/references/customize-local/change-spec-structure.md b/.pi/skills/trellis-meta/references/customize-local/change-spec-structure.md new file mode 100644 index 0000000..ee9a176 --- /dev/null +++ b/.pi/skills/trellis-meta/references/customize-local/change-spec-structure.md @@ -0,0 +1,83 @@ +# Change Local Spec Structure + +When the user wants to change the engineering conventions AI follows, add new spec layers, or adjust monorepo package mapping, edit `.trellis/spec/` and `.trellis/config.yaml`. + +## Read These Files First + +1. `.trellis/config.yaml` +2. `.trellis/spec/` +3. `.trellis/workflow.md` planning artifact guidance and Phase 3.3 +4. Current task `implement.jsonl` / `check.jsonl` + +## Common Needs + +| Need | Edit location | +| --- | --- | +| Add backend/frontend/docs/test spec layer | `.trellis/spec/<layer>/` or `.trellis/spec/<package>/<layer>/` | +| Add shared thinking guides | `.trellis/spec/guides/` | +| Adjust monorepo packages | `packages` in `.trellis/config.yaml` | +| Change default package | `default_package` in `.trellis/config.yaml` | +| Control spec scanning scope | `spec_scope` in `.trellis/config.yaml` | +| Make a task read a new spec | Task `implement.jsonl` / `check.jsonl` | + +## Add A Spec Layer + +Single-repository example: + +```text +.trellis/spec/security/ +├── index.md +└── auth.md +``` + +Monorepo example: + +```text +.trellis/spec/webapp/security/ +├── index.md +└── auth.md +``` + +`index.md` should include: + +- What code this layer applies to. +- Pre-Development Checklist. +- Quality Check. +- Links to specific guideline files. + +## Update Context + +Adding a spec does not mean every task automatically reads it. The current task must reference it in JSONL: + +```bash +python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/webapp/security/index.md" "Security conventions" +python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/webapp/security/index.md" "Security review rules" +``` + +## Change Monorepo Packages + +Example `.trellis/config.yaml`: + +```yaml +packages: + webapp: + path: apps/web + api: + path: apps/api +default_package: webapp +``` + +After editing, run: + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Use this output to confirm AI can see the correct packages and spec layers. + +## Notes + +- Specs are user project conventions and can be changed according to project needs. +- Do not put temporary task information into specs; put temporary information in the task. +- Do not put long-term conventions only in agents or commands; preserve them in specs. +- After changing spec structure, check whether existing task JSONL files still point to files that exist. diff --git a/.pi/skills/trellis-meta/references/customize-local/change-task-lifecycle.md b/.pi/skills/trellis-meta/references/customize-local/change-task-lifecycle.md new file mode 100644 index 0000000..a7a340f --- /dev/null +++ b/.pi/skills/trellis-meta/references/customize-local/change-task-lifecycle.md @@ -0,0 +1,90 @@ +# Change Local Task Lifecycle + +Task lifecycle includes creation, start, context configuration, finish, archive, parent/child tasks, and lifecycle hooks. The default customization targets are `.trellis/tasks/`, `.trellis/config.yaml`, and `.trellis/scripts/`. + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/config.yaml` +3. `.trellis/scripts/task.py` +4. `.trellis/scripts/common/task_store.py` +5. `.trellis/scripts/common/task_utils.py` +6. The current task's `.trellis/tasks/<task>/task.json` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Automatically sync an external system after task creation | `hooks.after_create` in `.trellis/config.yaml`. | +| Automatically update status after task start | `hooks.after_start` in `.trellis/config.yaml`. | +| Run a script after task finish | `hooks.after_finish` in `.trellis/config.yaml`. | +| Clean external resources after archive | `hooks.after_archive` in `.trellis/config.yaml`. | +| Change default task fields | `.trellis/scripts/common/task_store.py`. | +| Change task parsing/search | `.trellis/scripts/common/task_utils.py`. | +| Change active task behavior | `.trellis/scripts/common/active_task.py`. | + +## lifecycle hooks + +`.trellis/config.yaml` supports: + +```yaml +hooks: + after_create: + - "python3 .trellis/scripts/hooks/my_sync.py create" + after_start: + - "python3 .trellis/scripts/hooks/my_sync.py start" + after_finish: + - "python3 .trellis/scripts/hooks/my_sync.py finish" + after_archive: + - "python3 .trellis/scripts/hooks/my_sync.py archive" +``` + +Hook commands receive the `TASK_JSON_PATH` environment variable, pointing to the current task's `task.json`. Hook failures should usually warn, but not block the main task operation. + +## Change Task Fields + +If the user wants to add project-local fields, prefer putting them under `meta` in `task.json` to avoid breaking existing scripts' assumptions about standard fields. + +Example: + +```json +"meta": { + "linearIssue": "ENG-123", + "risk": "high" +} +``` + +If standard fields really need to change, inspect every local script that reads `task.json`. + +## Change Active Task + +Active task is session-level state stored in `.trellis/.runtime/sessions/`. Do not fall back to a global `.current-task` model. If the user wants to change active task behavior, edit: + +- `.trellis/scripts/common/active_task.py` +- platform hooks or shell session bridges +- active task descriptions in `.trellis/workflow.md` + +### `task.py create` Sets the Active Pointer + +`cmd_create` in `.trellis/scripts/common/task_store.py` calls `set_active_task` best-effort right after writing the new task directory. The behavior: + +- When the calling shell carries session identity (`TRELLIS_CONTEXT_ID` env var, or any platform-specific session env that `resolve_context_key` recognizes — see `active_task.py:_ENV_SESSION_KEYS`), the per-session pointer at `.trellis/.runtime/sessions/<context_key>.json` is rewritten to point at the new task. The task's `status=planning` and `[workflow-state:planning]` fires on the very next `UserPromptSubmit`. +- When session identity is unavailable (raw CLI invocation outside an AI session, or a platform that doesn't propagate identity to shell), the task directory is still created and `status=planning` is still written, but the active pointer is left untouched. The user can attach the task later with `task.py start <dir>` once they're back in an AI session. + +This makes `[workflow-state:planning]` the live breadcrumb during the brainstorm and JSONL curation work that follows `task.py create`. The pre-R7 behavior left the breadcrumb stuck on `no_task` until `task.py start`, so the planning block was effectively dead text. + +If you fork `task.py` to add a new creation path (e.g. an external import that bypasses `cmd_create`), audit whether your path also calls `set_active_task`. Without that call, your created tasks will not surface as active. The full status writer table is in `.trellis/spec/cli/backend/workflow-state-contract.md`. + +## Modification Steps + +1. Confirm the current task with `python3 ./.trellis/scripts/task.py current --source`. +2. Read the current task's `task.json` and confirm status and fields. +3. For configuration needs, edit `.trellis/config.yaml` first. +4. For script behavior needs, then edit `.trellis/scripts/`. +5. If the AI flow changed, synchronize `.trellis/workflow.md`. + +## Do Not + +- Do not directly edit `.trellis/.runtime/sessions/` to "fix" business state. +- Do not hard-code project-private fields into scripts; prefer `meta`. +- Do not default to asking the user to fork Trellis CLI. diff --git a/.pi/skills/trellis-meta/references/customize-local/change-workflow.md b/.pi/skills/trellis-meta/references/customize-local/change-workflow.md new file mode 100644 index 0000000..337c985 --- /dev/null +++ b/.pi/skills/trellis-meta/references/customize-local/change-workflow.md @@ -0,0 +1,65 @@ +# Change Local Workflow + +When the user wants to change Trellis phases, next-action hints, whether to create tasks, whether to use sub-agents, or when to check/wrap up, edit `.trellis/workflow.md` first. + +## Read These Files First + +1. `.trellis/workflow.md` +2. Entry files for the current platform, such as skills/commands/prompts/workflows +3. The current task's `task.json` and `prd.md` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Change phase names or phase order | `Phase Index` and the corresponding Phase sections. | +| Change whether to create a task when there is no task | `[workflow-state:no_task]` state block. | +| Change the next step during planning | Phase 1 and `[workflow-state:planning]`. | +| Change whether an agent is required during in_progress | Phase 2 and `[workflow-state:in_progress]`. | +| Change wrap-up after completion | Phase 3 and `[workflow-state:completed]`. | +| Change which skill a user intent triggers | `Skill Routing` table. | + +## Modification Steps + +1. Find the relevant section in `.trellis/workflow.md`. +2. When changing rules, keep explicit trigger conditions and next actions. +3. If adding or renaming a skill/agent, synchronize the corresponding files in platform directories. +4. Workflow-state changes only need an edit to the `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook is parser-only — it reads whatever you put in the block. Keep the opening and closing tags' STATUS strings identical (`[workflow-state:foo]…[/workflow-state:foo]`); mismatched STATUS pairs are silently dropped. +5. Make the AI reread `.trellis/workflow.md`; do not keep using rules from the old conversation. + +## Example: Relax Task Creation Requirements + +To change when task creation can be skipped, usually edit `[workflow-state:no_task]`: + +```md +[workflow-state:no_task] +Task is not required when the answer is a one-reply explanation, no files are changed, and no research is needed. +[/workflow-state:no_task] +``` + +If the formal Phase 1 flow also needs to change, synchronize the Phase 1 section. + +## Example: One Platform Does Not Use Sub-Agents + +If the user wants only one platform to avoid sub-agents, first confirm whether that platform has a separate group in the workflow. Then change Phase 2 routing for that platform group instead of deleting all `trellis-implement` / `trellis-check` instructions across platforms. + +## `/trellis:continue` Route Table + +`/trellis:continue` resumes a task by deciding which phase step to load next. The decision combines `task.json.status` with the presence of artifacts inside the task directory. The mapping is fixed in the command itself; forks that add custom statuses must extend both the workflow.md tag block and this table. + +| `status` | Artifact state | Resume at | +| --- | --- | --- | +| `planning` | `prd.md` missing | Phase 1.1 (load `trellis-brainstorm`) | +| `planning` | 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.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. + +## Notes + +`.trellis/workflow.md` is the local project workflow, not an immutable template. The user can adapt it to team habits. After editing it, platform entry files may still contain old descriptions, so inspect them too. diff --git a/.pi/skills/trellis-meta/references/customize-local/overview.md b/.pi/skills/trellis-meta/references/customize-local/overview.md new file mode 100644 index 0000000..b75d208 --- /dev/null +++ b/.pi/skills/trellis-meta/references/customize-local/overview.md @@ -0,0 +1,55 @@ +# Local Customization Overview + +This directory is for local AI working in a user project where Trellis was installed through npm and `trellis init` has already been run. The AI should modify generated `.trellis/` and platform directories inside the project, not Trellis CLI upstream source code. + +## First Determine What The User Actually Wants To Change + +| User wording | Read first | +| --- | --- | +| "Change the Trellis flow / phases / next prompt" | `change-workflow.md` | +| "Change task creation, status, archive, or hooks" | `change-task-lifecycle.md` | +| "AI did not read context / change injected content" | `change-context-loading.md` | +| "A platform hook is not behaving as expected" | `change-hooks.md` | +| "Change implement/check/research agent behavior" | `change-agents.md` | +| "Add a skill/command/workflow/prompt" | `change-skills-or-commands.md` | +| "Adjust the project spec structure" | `change-spec-structure.md` | +| "Add team conventions and local notes" | `add-project-local-conventions.md` | + +## General Operation Order + +1. **Confirm platform and directories**: inspect which directories exist, such as `.claude/`, `.codex/`, `.cursor/`, `.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. +5. **Synchronize semantics**: if a shared flow changes, check whether platform entry points also need changes; if a platform entry changes, check whether `.trellis/workflow.md` still agrees. + +## Local File Priority + +| Layer | Files | +| --- | --- | +| Workflow | `.trellis/workflow.md` | +| Project configuration | `.trellis/config.yaml` | +| Task material | `.trellis/tasks/<task>/` | +| Project specs | `.trellis/spec/` | +| Runtime scripts | `.trellis/scripts/` | +| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.zcode/`, and similar directories | +| Shared skill | `.agents/skills/` | + +## Things Not To Do By Default + +- Do not edit the global npm install directory. +- Do not edit `node_modules/@mindfoldhq/trellis`. +- Do not assume the user has the Trellis GitHub repository. +- Do not overwrite local files already modified by the user with default templates. +- Do not put team project rules into public `trellis-meta`; project rules belong in `.trellis/spec/` or a local skill. + +## When To Inspect Upstream Source + +Switch to an upstream source-code perspective only when the user explicitly expresses one of these goals: + +- "I want to open a PR to Trellis" +- "I want to change npm package publish contents" +- "I want to fork Trellis" +- "I want to modify the generation logic for `trellis init/update`" + +Otherwise, default to modifying local Trellis files inside the user project. diff --git a/.pi/skills/trellis-meta/references/local-architecture/bundled-skills.md b/.pi/skills/trellis-meta/references/local-architecture/bundled-skills.md new file mode 100644 index 0000000..1c819cb --- /dev/null +++ b/.pi/skills/trellis-meta/references/local-architecture/bundled-skills.md @@ -0,0 +1,146 @@ +# 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` | +| 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`, 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/.pi/skills/trellis-meta/references/local-architecture/context-injection.md b/.pi/skills/trellis-meta/references/local-architecture/context-injection.md new file mode 100644 index 0000000..4a7517b --- /dev/null +++ b/.pi/skills/trellis-meta/references/local-architecture/context-injection.md @@ -0,0 +1,68 @@ +# Local Context Injection System + +Trellis context injection aims to make AI read the right files at the right time instead of relying on model memory. In a user project, injection is implemented by `.trellis/` scripts together with platform hooks, agents, and skills. + +## Injected Context Types + +| Type | Source | Purpose | +| --- | --- | --- | +| session context | `.trellis/scripts/get_context.py` | Current developer, git status, active task, active tasks, journal, packages. | +| workflow context | `.trellis/workflow.md` | Current Trellis flow and next action. | +| spec context | `.trellis/spec/` + task JSONL | Specs that must be followed during implementation/checking. | +| task context | `.trellis/tasks/<task>/prd.md`, `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 + +Platforms with session-start support inject a Trellis overview when a session starts, clears, compacts, or receives a similar event. Injected content usually includes: + +- workflow summary. +- current task status. +- active tasks. +- spec index paths. +- developer identity and git status. + +If the user feels the AI does not know the current task in a new session, first check whether the platform's session-start hook or equivalent mechanism is installed and running. + +## workflow-state + +workflow-state is a lightweight hint injected around each user turn. Based on current task status, it selects a block from `.trellis/workflow.md`, such as `no_task`, `planning`, `in_progress`, or `completed`. + +If the user wants to change "what the AI should do next in a given state," edit the corresponding state block in `.trellis/workflow.md` first. + +## sub-agent context + +Implement and check agents need task context. Trellis has two loading modes: + +1. **hook push**: a platform hook injects 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 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 + +`implement.jsonl` and `check.jsonl` contain one JSON object per line: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend rules"} +``` + +Readers should skip seed rows without a `file` field. When configuring JSONL, the AI should include only spec/research files, not pre-register code files that will be modified. + +## Active Task And Context Key + +Active task state lives in `.trellis/.runtime/sessions/` and is isolated per session. Hooks try to resolve the context key from platform events, environment variables, transcript paths, or `TRELLIS_CONTEXT_ID`. + +If shell commands cannot see the same context key, `task.py current --source` may report no active task. In that case, check whether the platform passes session identity into the shell instead of hand-writing a global current-task file. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change session-start injected content | The platform's `session-start` hook or plugin file. | +| Change per-turn workflow-state rules | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The platform workflow-state hook parses these blocks verbatim and embeds no fallback text. | +| Change how sub-agents read context | Platform agent definitions, the `inject-subagent-context` hook, or agent preludes. | +| Change JSONL validation/display | `.trellis/scripts/common/task_context.py`. | +| Change active task resolution | `.trellis/scripts/common/active_task.py`. | + +When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct task artifacts/spec/research. diff --git a/.pi/skills/trellis-meta/references/local-architecture/generated-files.md b/.pi/skills/trellis-meta/references/local-architecture/generated-files.md new file mode 100644 index 0000000..5e53865 --- /dev/null +++ b/.pi/skills/trellis-meta/references/local-architecture/generated-files.md @@ -0,0 +1,80 @@ +# Local Files Generated After Init + +`trellis init` writes the Trellis runtime into the user project. Later, `trellis update` tries to update Trellis-managed template files, but it uses `.trellis/.template-hashes.json` to determine which files have already been modified by the user. + +This page only describes files that are visible and editable inside the user project. + +## `.trellis/` + +```text +.trellis/ +├── workflow.md +├── config.yaml +├── .developer +├── .version +├── .template-hashes.json +├── .runtime/ +├── scripts/ +├── spec/ +├── tasks/ +└── workspace/ +``` + +| Path | Usually editable? | Notes | +| --- | --- | --- | +| `.trellis/workflow.md` | Yes | Local workflow documentation and AI routing rules. | +| `.trellis/config.yaml` | Yes | Project configuration, hooks, packages, journal line limits, and related settings. | +| `.trellis/spec/` | Yes | Project specs, intended to be updated regularly by users and AI. | +| `.trellis/tasks/` | Yes | Task material and research artifacts, maintained by the task workflow. | +| `.trellis/workspace/` | Yes | Session records, usually written by `add_session.py`. | +| `.trellis/scripts/` | Carefully | Local runtime. It can be customized, but only after understanding the call chain. | +| `.trellis/.runtime/` | No | Runtime state, usually written automatically by hooks/scripts. | +| `.trellis/.developer` | Carefully | Current developer identity. | +| `.trellis/.version` | No | Trellis version record used by update/migration logic. | +| `.trellis/.template-hashes.json` | No | Template hash record. Do not hand-write business rules here. | + +## Platform Directories + +Different platforms generate different directories. Common categories: + +| Category | Example paths | Purpose | +| --- | --- | --- | +| hooks | `.claude/hooks/`, `.codex/hooks/`, `.cursor/hooks/` | Inject session context, workflow-state, and sub-agent context. | +| settings | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.trae/hooks.json` | Tell the platform when to run hooks or plugins. | +| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/`, `.zcode/cli/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/`, `.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. + +## Meaning Of Template Hashes + +`.trellis/.template-hashes.json` records the content hash from the last time Trellis wrote a template file. `trellis update` uses it to distinguish three cases: + +| Case | Update behavior | +| --- | --- | +| File was not modified by the user | It can be updated automatically. | +| File was modified by the user | Prompt the user to overwrite, keep, or generate `.new`. | +| File is no longer a current template | It may be deleted, renamed, or preserved according to migration rules. | + +When an AI customizes local Trellis files, it does not need to maintain hashes manually. It is normal for Trellis update to recognize the result as "modified by the user." + +## Local Customization Boundaries + +Editable by default: + +- `.trellis/workflow.md` +- `.trellis/config.yaml` +- `.trellis/spec/**` +- `.trellis/scripts/**` +- Platform hooks, settings, agents, skills, commands, prompts, and workflows + +Do not edit by default: + +- Global npm install directory +- `node_modules/@mindfoldhq/trellis` +- Trellis GitHub repository source code +- Concrete state files under `.trellis/.runtime/**` +- Hash contents inside `.trellis/.template-hashes.json` + +Switch to the Trellis CLI source-code perspective only when the user explicitly wants to contribute upstream. diff --git a/.pi/skills/trellis-meta/references/local-architecture/multi-agent-channel.md b/.pi/skills/trellis-meta/references/local-architecture/multi-agent-channel.md new file mode 100644 index 0000000..6df61eb --- /dev/null +++ b/.pi/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/.pi/skills/trellis-meta/references/local-architecture/overview.md b/.pi/skills/trellis-meta/references/local-architecture/overview.md new file mode 100644 index 0000000..e97cab8 --- /dev/null +++ b/.pi/skills/trellis-meta/references/local-architecture/overview.md @@ -0,0 +1,51 @@ +# Local Trellis Architecture Overview + +`trellis-meta` is for user projects that have already run `trellis init`. The user's machine usually has only the npm-installed `trellis` command plus the Trellis files generated inside the project; it may not have the Trellis CLI source code. + +Therefore, when an AI uses this skill, the default customization target is local files inside the user project: + +- `.trellis/`: workflow, tasks, specs, memory, scripts, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.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. + +## Local System Model + +Trellis provides three layers inside a user project: + +1. **Workflow layer**: `.trellis/workflow.md` defines phases, routing, next actions, and prompt blocks. +2. **Persistence layer**: `.trellis/tasks/`, `.trellis/spec/`, and `.trellis/workspace/` store tasks, specs, and session memory. +3. **Platform integration layer**: hooks, settings, agents, skills, commands, prompts, and workflows in platform directories connect the Trellis workflow to different AI tools. + +All three layers live inside the user project, so an AI can read and modify them directly. + +## Core Paths + +| Path | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow phases, skill routing, and workflow-state prompt blocks. | +| `.trellis/config.yaml` | Project configuration, task lifecycle hooks, monorepo package configuration, and journal configuration. | +| `.trellis/spec/` | The user's project-specific coding conventions and thinking guides. | +| `.trellis/tasks/` | Each task's PRD, technical notes, research files, and JSONL context. | +| `.trellis/workspace/` | Per-developer journals and cross-session memory. | +| `.trellis/scripts/` | Local Python runtime used by commands, hooks, and context injection. | +| `.trellis/.runtime/` | Session-level runtime state, such as the current task pointer. | +| `.trellis/.template-hashes.json` | Template hashes for Trellis-managed files, used by update to determine whether local files were modified by the user. | + +## AI Customization Principles + +1. **Find the local source of truth first**: Do not edit from memory. Read `.trellis/workflow.md`, `.trellis/config.yaml`, the relevant platform directory, and related task files first. +2. **Edit the user project, not the npm package cache**: Modify generated files inside the project, not `node_modules` or the global npm install directory. +3. **Keep platform files aligned with `.trellis/`**: If workflow routing changes, also check whether platform skills or commands still describe the same flow. +4. **Put project-specific rules in `.trellis/spec/` or a local skill**: Do not put team conventions into `trellis-meta`. +5. **Preserve user changes**: If a file was already modified locally, work from the current content instead of overwriting it with a default template. + +## How To Use This Directory + +- To understand which files exist after init, read `generated-files.md`. +- To change phases, routing, or next actions, read `workflow.md`. +- To change the task model, JSONL context, or active task behavior, read `task-system.md`. +- To change coding convention injection, read `spec-system.md`. +- To understand journals and cross-session memory, read `workspace-memory.md`. +- To change hooks or sub-agent context loading, read `context-injection.md`. diff --git a/.pi/skills/trellis-meta/references/local-architecture/spec-system.md b/.pi/skills/trellis-meta/references/local-architecture/spec-system.md new file mode 100644 index 0000000..38fdf14 --- /dev/null +++ b/.pi/skills/trellis-meta/references/local-architecture/spec-system.md @@ -0,0 +1,102 @@ +# Local Spec System + +`.trellis/spec/` is the user's project-specific engineering spec library. Trellis is not about making AI memorize conventions; it injects relevant specs or requires the AI to read them at the right time. + +## Directory Model + +A common single-repository structure: + +```text +.trellis/spec/ +├── backend/ +│ ├── index.md +│ └── ... +├── frontend/ +│ ├── index.md +│ └── ... +└── guides/ + ├── index.md + └── ... +``` + +A common monorepo structure: + +```text +.trellis/spec/ +├── cli/ +│ ├── backend/ +│ │ ├── index.md +│ │ └── ... +│ └── unit-test/ +│ ├── index.md +│ └── ... +├── docs-site/ +│ └── docs/ +│ ├── index.md +│ └── ... +└── guides/ + ├── index.md + └── ... +``` + +`index.md` is the entry point for each layer. It should list the Pre-Development Checklist and Quality Check. Specific guidelines live in other Markdown files in the same directory. + +## Package Configuration + +`.trellis/config.yaml` can declare packages: + +```yaml +packages: + cli: + path: packages/cli + docs-site: + path: docs-site + type: submodule +default_package: cli +``` + +The AI can run: + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +This command lists packages and spec layers for the current project. Use this output as the reference when configuring context JSONL. + +## How Specs Enter Tasks + +Before a task enters implementation, 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"} +{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Test expectations"} +``` + +Sub-agents or platform preludes read these JSONL files and load the referenced specs. On platforms without sub-agent support, the AI should read the relevant specs directly according to the workflow. + +## What Specs Should Contain + +Specs should contain executable engineering conventions for the project, not generic best practices: + +- Where files should live. +- How error handling should be expressed. +- Input/output contracts for APIs, hooks, and commands. +- Patterns that are forbidden. +- Cases that require tests. +- Project-specific pitfalls and how to avoid them. + +When the AI learns a new rule during implementation or debugging, it should update `.trellis/spec/` rather than only summarizing it in chat. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Add a new spec layer | `.trellis/spec/<package>/<layer>/index.md` and corresponding guideline files. | +| Change monorepo spec mapping | `packages` / `default_package` / `spec_scope` in `.trellis/config.yaml`. | +| Change which specs AI reads before implementation | The task's `implement.jsonl`. | +| Change which specs AI reads during checking | The task's `check.jsonl`. | +| Change when specs should be updated | Phase 3.3 in `.trellis/workflow.md` and the `trellis-update-spec` skill. | + +## Boundaries + +`.trellis/spec/` is the user's project specification, not a permanent copy of Trellis built-in templates. The AI should encourage the user to update it according to the actual project code instead of treating Trellis default templates as immutable documents. diff --git a/.pi/skills/trellis-meta/references/local-architecture/task-system.md b/.pi/skills/trellis-meta/references/local-architecture/task-system.md new file mode 100644 index 0000000..7133495 --- /dev/null +++ b/.pi/skills/trellis-meta/references/local-architecture/task-system.md @@ -0,0 +1,130 @@ +# Local Task System + +The Trellis task system is stored entirely under `.trellis/tasks/` in the user project. Each task is a directory containing requirements, context, research, state, and relationship information. + +## Task Directory Structure + +```text +.trellis/tasks/ +├── 04-28-example-task/ +│ ├── task.json +│ ├── prd.md +│ ├── design.md +│ ├── implement.md +│ ├── implement.jsonl +│ ├── check.jsonl +│ └── research/ +└── archive/ + └── 2026-04/ +``` + +| File | Purpose | +| --- | --- | +| `task.json` | Task metadata: status, assignee, priority, branch, parent/child tasks, and similar fields. | +| `prd.md` | Requirements, 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. | + +## `task.json` + +`task.json` records task status and metadata. Common fields: + +| Field | Meaning | +| --- | --- | +| `id` / `name` / `title` | Task identity and title. | +| `status` | Status such as `planning`, `in_progress`, `review`, or `completed`. | +| `priority` | `P0`, `P1`, `P2`, `P3`. | +| `creator` / `assignee` | Creator and assignee. | +| `package` | Target package in a monorepo; may be empty. | +| `branch` / `base_branch` | Working branch and PR target branch. | +| `children` / `parent` | Parent/child task relationships. | +| `commit` / `pr_url` | Commit and PR information after completion. | +| `meta` | Extension fields. | + +## 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 + +The user sees a "current task," but Trellis stores active task state per session. + +```text +.trellis/.runtime/sessions/<context-key>.json +``` + +`task.py start` writes the task path into the runtime session file for the current session. `task.py current --source` shows the current task and where it came from. Different AI windows can point to different tasks without overwriting each other. + +If the platform or shell environment has no stable session identity, `task.py start` may be unable to set the active task. The AI should read the error, inspect the platform hook/session environment, and not fall back to a shared global pointer. + +## JSONL Context + +`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first. They do not replace `implement.md`; `implement.md` is the human-readable execution plan. + +Format: + +```jsonl +{"file": ".trellis/spec/cli/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-example/research/api.md", "reason": "API research"} +``` + +Rules: + +- Include spec and research files. +- Do not include code files that are about to be modified. +- Do not treat temporary conclusions in chat as the only context. +- Seed rows have no `file` field; they only prompt the AI to fill in real entries. + +## Common Commands + +```bash +python3 ./.trellis/scripts/task.py create "<title>" --slug <slug> +python3 ./.trellis/scripts/task.py start <task> +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py add-context <task> implement <file> <reason> +python3 ./.trellis/scripts/task.py validate <task> +python3 ./.trellis/scripts/task.py finish +python3 ./.trellis/scripts/task.py archive <task> +``` + +When modifying the task system, the AI should prefer script commands to maintain structure. Edit JSON/Markdown directly only when scripts do not cover the need. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change the default task template | `.trellis/scripts/common/task_store.py` and task creation instructions. | +| Change status semantics | `.trellis/workflow.md`, workflow-state hook logic, and task usage conventions. | +| Add task lifecycle actions | `hooks.after_*` in `.trellis/config.yaml`. | +| Change context rules | 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/.pi/skills/trellis-meta/references/local-architecture/workflow.md b/.pi/skills/trellis-meta/references/local-architecture/workflow.md new file mode 100644 index 0000000..f0659ff --- /dev/null +++ b/.pi/skills/trellis-meta/references/local-architecture/workflow.md @@ -0,0 +1,75 @@ +# Local Workflow System + +`.trellis/workflow.md` is the Trellis workflow source of truth inside the user project. An AI does not need Trellis source code to understand how the current project should move tasks forward; this file is enough. + +## File Responsibilities + +`.trellis/workflow.md` has three responsibilities: + +1. **Explain workflow phases**: Plan, Execute, Finish. +2. **Define skill routing**: which skill or agent the AI should use when the user expresses a certain intent. +3. **Provide workflow-state prompt blocks**: hooks can inject the prompt block for the current state into the conversation. + +## Current Phase Model + +```text +Phase 1: Plan -> clarify what to build, produce prd.md and required research +Phase 2: Execute -> implement against the PRD and specs, then check +Phase 3: Finish -> final verification, preserve lessons, and wrap up +``` + +Each phase contains numbered steps, such as `1.3 Configure context`. These numbers are not runtime fields in `task.json`; they are workflow structure for AI and humans to read. + +## Skill Routing + +`workflow.md` separates routing by platform capability: + +- Platforms with sub-agent support: dispatch `trellis-implement` by default for implementation and `trellis-check` for checking. +- Platforms without sub-agent support: the main session reads skills such as `trellis-before-dev`, then executes directly. + +When changing local AI behavior, update the routing descriptions in `workflow.md` first, then check whether the corresponding platform skill, command, or agent files need to stay in sync. + +## Workflow-State Prompt Blocks + +The bottom of `workflow.md` can contain state blocks like this: + +```text +[workflow-state:no_task] +... +[/workflow-state:no_task] +``` + +Hooks choose the right block based on current task status and inject it into the conversation. Common states include: + +| State | Meaning | +| --- | --- | +| `no_task` | The current session has no active task. | +| `planning` | The task is still in requirements, research, or context configuration. | +| `in_progress` | The task has entered implementation and checking. | +| `completed` | The task is complete and waiting for wrap-up or archive. | + +If the user wants to change policies such as "whether to create a task when there is no task," "when task creation may be skipped," or "whether sub-agents are required," edit these state blocks and the routing table above them. + +## Local Modification Patterns + +Common changes: + +| Goal | Edit point | +| --- | --- | +| Add a phase | Update the Phase Index, phase body, routing, and state blocks. | +| Change task creation policy | Update the `no_task` state block and Phase 1 description. | +| Change the default implementation/check path | Update Phase 2 and skill routing. | +| Change the wrap-up flow | Update Phase 3 and `finish-work` related descriptions. Note the current split: Phase 3.4 = AI-driven code commits (batched, user-confirmed), Phase 3.5 = `/finish-work` (archive + record session). `/finish-work` refuses to run if the working tree is dirty. | +| Change platform differences | Update routing descriptions grouped by platform. | + +After editing, make the AI reread `.trellis/workflow.md`; do not assume the flow from the old conversation is still valid. + +## Relationship To Platform Files + +`workflow.md` is the semantic center of the local workflow, but each platform can also have its own entry files: + +- skills, such as `trellis-brainstorm` and `trellis-check`. +- commands/prompts/workflows, such as continue and finish-work. +- hooks, such as session-start or workflow-state injection. + +If only `workflow.md` changes, platform entry files may still contain old language. When the user wants to change "what the AI actually does," also inspect the relevant platform directory. diff --git a/.pi/skills/trellis-meta/references/local-architecture/workspace-memory.md b/.pi/skills/trellis-meta/references/local-architecture/workspace-memory.md new file mode 100644 index 0000000..c2958f2 --- /dev/null +++ b/.pi/skills/trellis-meta/references/local-architecture/workspace-memory.md @@ -0,0 +1,71 @@ +# Local Workspace Memory System + +`.trellis/workspace/` stores cross-session memory. Its purpose is to let AI and humans understand what happened before across different windows and different days. + +## Directory Structure + +```text +.trellis/workspace/ +├── index.md +└── <developer>/ + ├── index.md + ├── journal-1.md + └── journal-2.md +``` + +| File | Purpose | +| --- | --- | +| `.trellis/.developer` | Current developer identity. | +| `.trellis/workspace/index.md` | Global workspace overview. | +| `.trellis/workspace/<developer>/index.md` | Session index for a developer. | +| `.trellis/workspace/<developer>/journal-N.md` | Session journal. | + +## Developer Identity + +Run this the first time: + +```bash +python3 ./.trellis/scripts/init_developer.py <name> +``` + +This creates `.trellis/.developer` and the corresponding workspace directory. The AI should not change developer identity casually; if the identity is wrong, first confirm who is using the current project. + +## Journal + +`journal-N.md` records completed or partially completed work from each session. By default, each journal holds about 2000 lines; after that it rotates to the next file. + +Common command for recording a session: + +```bash +python3 ./.trellis/scripts/add_session.py \ + --title "Session title" \ + --summary "What changed" \ + --commit "abc1234" +``` + +Planning or review work without a commit can also be recorded by using `--no-commit` or an empty commit value. + +## Relationship Between Workspace Memory And Tasks + +| System | What it stores | +| --- | --- | +| `.trellis/tasks/` | Requirements, design, research, and state for a specific task. | +| `.trellis/workspace/` | Work records across tasks and sessions. | +| `.trellis/spec/` | Engineering knowledge preserved as long-term conventions. | + +If information is only useful for the current task, put it in the task directory. +If information describes what happened in the current session, put it in the workspace journal. +If information should be followed every time code is written in the future, put it in spec. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change maximum journal lines | `max_journal_lines` in `.trellis/config.yaml`. | +| Change session auto-commit message | `session_commit_message` in `.trellis/config.yaml`. | +| Change session content format | `.trellis/scripts/add_session.py`. | +| Change how workspace is displayed in context | `.trellis/scripts/common/session_context.py`. | + +## AI Usage Rules + +The AI should not treat workspace as the only source of truth. When resuming a task, read the current task first, then use workspace for background. After a task is complete, record important process notes in workspace; if long-term rules emerged, update spec. diff --git a/.pi/skills/trellis-meta/references/platform-files/agents.md b/.pi/skills/trellis-meta/references/platform-files/agents.md new file mode 100644 index 0000000..a07cae7 --- /dev/null +++ b/.pi/skills/trellis-meta/references/platform-files/agents.md @@ -0,0 +1,82 @@ +# Agents + +Trellis agent files define specialized roles. Common Trellis agents in a user project are: + +- `trellis-research` +- `trellis-implement` +- `trellis-check` + +File locations and formats differ by platform, but responsibility boundaries should stay consistent. + +## Agent Responsibilities + +| Agent | Responsibility | +| --- | --- | +| `trellis-research` | Investigate the question and write findings into the current task's `research/`. | +| `trellis-implement` | Implement against `prd.md`, 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. + +## Common Paths + +| Platform | Agent path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/cli/agents/trellis-*.md` | + +GitHub Copilot agent/prompt support is provided by a combination of directories such as `.github/agents/`, `.github/prompts/`, and `.github/skills/`; inspect the files actually generated in the user project. + +Main-session workflow platforms such as Kilo, Antigravity, and Devin may not have Trellis sub-agent files. They usually rely on workflows/skills to guide the main session. + +## Two Context Loading Modes + +### hook push + +The platform hook injects task context before the agent starts. The agent file itself can focus more on responsibilities and boundaries. + +Common on platforms that support agent hooks. + +### agent pull + +The agent file instructs the agent to read after startup: + +- `python3 ./.trellis/scripts/task.py current --source` +- `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. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Implement agent must follow extra restrictions | The platform's `trellis-implement` agent file. | +| Check agent must run project-specific commands | `trellis-check` agent file, and `.trellis/spec/` if needed. | +| Research agent must output a fixed format | `trellis-research` agent file. | +| Agent cannot read task context | Agent prelude or `inject-subagent-context` hook. | +| Add a project-specific agent | Platform agent directory + related workflow/command/skill entry point. | + +## Modification Principles + +1. **Keep responsibilities single-purpose**. Do not mix research, implement, and check responsibilities into one agent. +2. **Specify the read order**. Agents must know to start from the active task, 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. + +## Do Not Default To Editing Upstream Templates + +Local AI should default to modifying platform agent files inside the user project. Discuss upstream template source only when the user explicitly wants to contribute the change back to Trellis. diff --git a/.pi/skills/trellis-meta/references/platform-files/hooks-and-settings.md b/.pi/skills/trellis-meta/references/platform-files/hooks-and-settings.md new file mode 100644 index 0000000..d4ae8f6 --- /dev/null +++ b/.pi/skills/trellis-meta/references/platform-files/hooks-and-settings.md @@ -0,0 +1,72 @@ +# Hooks And Settings + +Hooks/settings are the entry layer that connects a platform to Trellis. They decide which scripts, plugins, or extensions a platform runs for which events. + +## Settings Responsibilities + +settings/config files usually register: + +- session-start hook: injects a Trellis overview when a new session starts or context resets. +- workflow-state hook: parses `[workflow-state:STATUS]` blocks from `.trellis/workflow.md` and emits the body matching the current task `status` on each user input. Parser-only; the script does not embed fallback content. +- sub-agent context hook: injects task context when implementation/check/research agents start. +- shell/session bridge: lets shell commands see the same Trellis session identity. +- platform plugin or extension entry points. + +Common files: + +| Platform | settings/config | +| --- | --- | +| Claude Code | `.claude/settings.json` | +| Cursor | `.cursor/hooks.json` | +| Codex | `.codex/hooks.json`, `.codex/config.toml` | +| OpenCode | `.opencode/package.json`, `.opencode/plugins/*` | +| Kiro | `.kiro/hooks/` + platform config | +| Gemini CLI | `.gemini/settings.json` | +| Qoder | `.qoder/settings.json` | +| CodeBuddy | `.codebuddy/settings.json` | +| GitHub Copilot | `.github/copilot/hooks.json` | +| Factory Droid | `.factory/settings.json` | +| Pi Agent | `.pi/settings.json`, `.pi/extensions/trellis/` | +| Trae IDE | `.trae/hooks.json` | + +Reasonix and ZCode are pull-based platforms that do not use hooks or settings files; their agent files contain prelude instructions to read context after startup. + +Whether these files exist in a project depends on which `trellis init --<platform>` flags the user ran. + +## Hook Script Types + +| Script | Purpose | +| --- | --- | +| `session-start.py` | Generates session-start context. | +| `inject-workflow-state.py` | Parses `[workflow-state:STATUS]` blocks in `.trellis/workflow.md` and emits the body matching the current task status. Falls back to `Refer to workflow.md for current step.` when no matching block exists. | +| `inject-subagent-context.py` | Injects PRD, JSONL context, and related spec/research into sub-agents. | +| `inject-shell-session-context.py` | Lets shell commands inherit Trellis session identity. | + +Not every platform has every hook. Do not copy files from another platform just because a platform lacks a hook; first confirm whether that platform supports the corresponding event. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| AI should see more/less context in a new session | Platform `session-start` hook. | +| Per-turn hint policy should change | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook parses workflow.md verbatim — no script edit required. | +| Sub-agent cannot read PRD/spec | `inject-subagent-context` hook or agent prelude. | +| `task.py current` in shell has no active task | Shell/session bridge hook or platform environment variable configuration. | +| Disable an automatic injection | The corresponding hook registration in settings/config. | + +## Modification Principles + +1. **Settings wire things up; hooks define behavior**. If only the hook changes, the platform may never call it. If only settings change, behavior may not change. +2. **Confirm platform event names first**. Different platforms use different names for SessionStart, UserPromptSubmit, AgentSpawn, shell execution, and similar events. +3. **Hooks read local `.trellis/`, not upstream source**. `.trellis/scripts/` and `.trellis/workflow.md` in the user project are the default targets. +4. **Errors must be visible**. Hook failures should tell the user what was not injected instead of silently leaving the AI without context. + +## Troubleshooting Path + +If the user says "AI did not read Trellis state": + +1. Check whether the platform settings register the hook. +2. Check whether the hook file exists. +3. Manually run the `.trellis/scripts/get_context.py` or `task.py current --source` command that the hook depends on. +4. Check whether active task state exists in `.trellis/.runtime/sessions/`. +5. Check whether the platform shell passes session identity. diff --git a/.pi/skills/trellis-meta/references/platform-files/overview.md b/.pi/skills/trellis-meta/references/platform-files/overview.md new file mode 100644 index 0000000..e403858 --- /dev/null +++ b/.pi/skills/trellis-meta/references/platform-files/overview.md @@ -0,0 +1,59 @@ +# Platform Files Overview + +Trellis connects the same local architecture to different AI tools. `.trellis/` stores the shared runtime; platform directories store adapter files that define how each AI tool enters Trellis. + +When a local AI modifies Trellis, it should distinguish two file categories first: + +- **Shared files**: `.trellis/workflow.md`, `.trellis/tasks/`, `.trellis/spec/`, `.trellis/scripts/`. +- **Platform files**: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.trae/`, `.kilocode/`, `.agent/`, `.devin/`, `.reasonix/`, `.zcode/`, and similar directories. + +Platform files do not store business state. They let the corresponding AI tool read Trellis state, call Trellis scripts, and load Trellis skills/agents/hooks. + +## Platform File Categories + +| Category | Common paths | Purpose | +| --- | --- | --- | +| settings/config | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.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/`, `.devin/workflows/` | Entry points explicitly invoked by the user. | + +## Three Platform Integration Modes + +### 1. Hook / Extension Driven + +These platforms can trigger scripts or plugins on specific events and actively inject Trellis context into AI. + +Common capabilities: + +- session-start injection of a `.trellis/` overview. +- workflow-state hints for each user turn. +- PRD/spec/research injection when sub-agents start. +- Shell commands inheriting session identity. + +To change "when the AI knows what," inspect hooks/plugins/extensions and settings first. + +### 2. Agent Prelude / Pull-Based + +Some platforms cannot reliably let hooks rewrite sub-agent prompts, so the agent file itself instructs the agent to read the active task, PRD, and JSONL context after startup. + +To change how sub-agents load context, inspect the agent files themselves. + +### 3. Main-Session Workflow + +Some platforms do not have Trellis sub-agent or hook capabilities. They rely on workflows/skills/commands to guide the main-session AI to read files, run scripts, and move tasks forward. + +To change behavior, inspect platform workflows/skills/commands and `.trellis/workflow.md`. + +## Local Modification Order + +When the user asks to customize behavior for a platform, the AI should inspect files in this order: + +1. Read `.trellis/workflow.md` to confirm the shared flow. +2. Read the target platform's settings/config to see which hooks/agents/skills/commands are registered. +3. Read the target platform's agents/skills/commands/hooks. +4. Modify the local file closest to the user's need. +5. If the change affects the shared flow, synchronize `.trellis/workflow.md` or `.trellis/spec/`. + +Do not modify only platform files and forget the shared workflow. Do not modify only `.trellis/workflow.md` and forget that platform entry points may still contain old descriptions. diff --git a/.pi/skills/trellis-meta/references/platform-files/platform-map.md b/.pi/skills/trellis-meta/references/platform-files/platform-map.md new file mode 100644 index 0000000..dddb743 --- /dev/null +++ b/.pi/skills/trellis-meta/references/platform-files/platform-map.md @@ -0,0 +1,88 @@ +# Platform File Map + +This page lists common Trellis file locations in a user project by platform. Whether a platform directory exists in an actual project depends on which `trellis init --<platform>` commands the user ran. + +## Matrix + +| Platform | CLI flag | Main directory | Skill directory | Agent directory | Hooks/extensions | +| --- | --- | --- | --- | --- | --- | +| Claude Code | `--claude` | `.claude/` | `.claude/skills/` | `.claude/agents/` | `.claude/hooks/` + `.claude/settings.json` | +| Cursor | `--cursor` | `.cursor/` | `.cursor/skills/` | `.cursor/agents/` | `.cursor/hooks.json` + `.cursor/hooks/` | +| OpenCode | `--opencode` | `.opencode/` | `.opencode/skills/` | `.opencode/agents/` | `.opencode/plugins/` | +| Codex | `--codex` | `.codex/` | `.agents/skills/` | `.codex/agents/` | `.codex/hooks/` + `.codex/hooks.json` | +| Kilo | `--kilo` | `.kilocode/` | `.kilocode/skills/` | Usually none | `.kilocode/workflows/` | +| Kiro | `--kiro` | `.kiro/` | `.kiro/skills/` | `.kiro/agents/` | `.kiro/hooks/` | +| Gemini CLI | `--gemini` | `.gemini/` | `.agents/skills/` | `.gemini/agents/` | `.gemini/settings.json` + `.gemini/hooks/` | +| Antigravity | `--antigravity` | `.agent/` | `.agent/skills/` | Usually none | `.agent/workflows/` | +| 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/` (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/` | `.agents/skills/` | `.zcode/cli/agents/` | pull-based prelude (no hooks) | + +## Capability Groups + +### Trellis Sub-Agent Support + +These platforms usually have `trellis-research`, `trellis-implement`, and `trellis-check` files: + +- Claude Code +- Cursor +- OpenCode +- Codex +- Kiro +- Gemini CLI +- Qoder +- CodeBuddy +- GitHub Copilot +- Factory Droid +- Pi Agent +- Trae IDE +- Reasonix (delivered as skills with `runAs: subagent` under `.reasonix/skills/`, not as a separate `agents/` directory) +- ZCode + +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 +- 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. + +## Decision Rules When Modifying Platform Files + +1. User specified a platform: modify only that platform directory unless shared workflow/spec files must also change. +2. User says "all platforms should do this": synchronize equivalent entry points platform by platform; do not modify only one directory. +3. User only says "my AI": inspect the configuration directories that actually exist in the project and infer the current AI platform. +4. User wants project rules: prefer `.trellis/spec/` or a project-local skill. +5. User wants Trellis behavior: edit `.trellis/workflow.md` plus platform hooks/agents/skills/commands. + +## When Paths Differ + +Platform ecosystems change, and user projects may already be customized. If this table disagrees with local files, use the actual settings/config in the user project as authoritative: + +- Check the hook that settings registers. +- Check the script that a command/prompt/workflow points to. +- Judge behavior by the read rules currently written in the agent file. + +Do not delete a custom file just because it is not listed in this path table. diff --git a/.pi/skills/trellis-meta/references/platform-files/skills-and-commands.md b/.pi/skills/trellis-meta/references/platform-files/skills-and-commands.md new file mode 100644 index 0000000..4732c6a --- /dev/null +++ b/.pi/skills/trellis-meta/references/platform-files/skills-and-commands.md @@ -0,0 +1,85 @@ +# Skills, Commands, Prompts, And Workflows + +Skills and commands are textual entry points for user interaction with Trellis. Different platforms use different names, but their core purpose is the same: tell the AI how to enter the Trellis flow when the user expresses a certain intent. + +## Conceptual Differences + +| Type | Trigger mode | Best for | +| --- | --- | --- | +| skill | AI auto-match or explicit user mention | Long-term capabilities, workflow rules, modification guides. | +| command | Explicit user invocation | Clear operation entry points such as continue and finish-work. | +| prompt | Explicit user invocation or platform selection | Similar to command, but in a platform prompt format. | +| workflow | Explicit user selection or platform auto-match | Guides the main session when no sub-agent/hook exists. | + +Trellis workflow skills usually share one semantic set: brainstorm, before-dev, check, update-spec, break-loop. Multi-file built-in skills such as `trellis-meta` use layered references. + +## Common Paths + +| Platform | Common entries | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| Kilo | `.kilocode/skills/`, `.kilocode/workflows/` | +| Kiro | `.kiro/skills/` | +| Gemini CLI | `.agents/skills/`, `.gemini/commands/` | +| Antigravity | `.agent/skills/`, `.agent/workflows/` | +| 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/` | +| Reasonix | `.reasonix/skills/` | +| ZCode | `.agents/skills/`, `.zcode/commands/` | + +In a user project, use the files actually generated by init as authoritative. + +## Skill Structure + +A common skill is a directory: + +```text +trellis-meta/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should tell the AI: + +- When to use this skill. +- Which reference to read first for the current task. +- What not to do. + +References hold longer explanations so the entry file does not contain everything. + +## Command/Prompt/Workflow Structure + +Commands, prompts, and workflows are usually single files. Their content should include: + +- When to use it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +They should not store task state; task state belongs in `.trellis/tasks/` and `.trellis/.runtime/`. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Change AI auto-trigger rules | The corresponding skill's frontmatter description. | +| Change user command behavior | The corresponding command/prompt/workflow file. | +| Add a project-local skill | Platform skill directory, or shared `.agents/skills/`. | +| Let multiple platforms share one capability | Write equivalent skills in each platform skill directory, or use the `.agents/skills/` shared layer on platforms that support it. | +| Change finish/continue entry points | Platform commands/prompts/workflows. | + +## Modification Principles + +1. **Keep entry files short; references carry long content**. This matters especially for multi-file skills like `trellis-meta`. +2. **Make trigger descriptions specific**. A description that is too broad can mis-trigger; one that is too narrow may not trigger. +3. **Keep the same semantics consistent across platforms**. File formats can differ, but behavior descriptions should match. +4. **Put project-specific capabilities in local skills**. Do not put team-private flows into public `trellis-meta`. + +If the user only wants local AI to know one more project rule, usually create a project-local skill or update `.trellis/spec/` instead of changing a Trellis built-in workflow skill. diff --git a/.pi/skills/trellis-session-insight/SKILL.md b/.pi/skills/trellis-session-insight/SKILL.md new file mode 100644 index 0000000..8e2cbb7 --- /dev/null +++ b/.pi/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, and Pi Agent conversation logs (the JSONL files each platform stores under `~/.claude/projects/`, `~/.codex/sessions/`, and `~/.pi/agent/sessions/`) and lets you list, search, slice by Trellis task boundaries, and dump cleaned dialogue from them. 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/.pi/skills/trellis-session-insight/references/cli-quick-reference.md b/.pi/skills/trellis-session-insight/references/cli-quick-reference.md new file mode 100644 index 0000000..78540f2 --- /dev/null +++ b/.pi/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/.pi/skills/trellis-session-insight/references/triggering-patterns.md b/.pi/skills/trellis-session-insight/references/triggering-patterns.md new file mode 100644 index 0000000..66021ca --- /dev/null +++ b/.pi/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/.pi/skills/trellis-spec-bootstrap/SKILL.md b/.pi/skills/trellis-spec-bootstrap/SKILL.md new file mode 100644 index 0000000..e1650df --- /dev/null +++ b/.pi/skills/trellis-spec-bootstrap/SKILL.md @@ -0,0 +1,41 @@ +--- +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 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. + +## Workflow + +1. Confirm Trellis is initialized and inspect the current `.trellis/spec/` tree. +2. Analyze the repository architecture with the best available tools: GitNexus, ABCoder, language tooling, and direct source reads. +3. Decompose the spec work by package and layer only when that reflects the actual codebase. +4. Fill or reshape the spec files with concrete patterns, file paths, examples, and anti-patterns from the project. +5. Verify that the final specs are internally consistent and contain no template placeholders. + +## Reference Routing + +| Need | Read | +|------|------| +| Repository architecture analysis | [references/repository-analysis.md](references/repository-analysis.md) | +| Spec work decomposition and task planning | [references/spec-task-planning.md](references/spec-task-planning.md) | +| Writing high-signal Trellis spec files | [references/spec-writing.md](references/spec-writing.md) | +| GitNexus and ABCoder MCP setup | [references/mcp-setup.md](references/mcp-setup.md) | + +## Operating Rules + +- Treat templates as starting points, not contracts. Delete, rename, split, or add spec files when the repository calls for it. +- Prefer source-backed rules over generic advice. Every important recommendation should point at a real file or repeated local pattern. +- Keep execution single-owner by default. Optional helper agents are an implementation detail, not a requirement or user-visible dependency. +- Do not write platform-specific instructions unless the target project already standardizes on that platform. +- Do not leave placeholder text, empty headings, or copied boilerplate in `.trellis/spec/`. + +## Done Criteria + +- `.trellis/spec/` describes the project as it exists now. +- Each relevant package or layer has practical coding guidance with real examples. +- Non-applicable template sections are removed. +- `index.md` files match the final spec file set. +- Any required setup or analysis assumptions are documented in the relevant spec or task notes. diff --git a/.pi/skills/trellis-spec-bootstrap/references/mcp-setup.md b/.pi/skills/trellis-spec-bootstrap/references/mcp-setup.md new file mode 100644 index 0000000..629fcbd --- /dev/null +++ b/.pi/skills/trellis-spec-bootstrap/references/mcp-setup.md @@ -0,0 +1,90 @@ +# MCP Setup + +GitNexus and ABCoder are recommended when bootstrapping Trellis specs because they expose architecture and AST context to the agent. They are tool choices, not platform requirements. Configure them through whatever MCP mechanism your agent host provides. + +## GitNexus + +GitNexus builds a code knowledge graph from the repository. Use it for module boundaries, execution flows, dependency relationships, blast radius, and graph queries. + +### Install and Index + +```bash +# Run from the repository root. +npx gitnexus analyze + +# Check index status. +npx gitnexus status + +# Re-index after code changes when the analysis is stale. +npx gitnexus analyze +``` + +The index is written to `.gitnexus/`. Keep embeddings only if the project already uses them; otherwise a normal index is enough for spec bootstrapping. + +### MCP Server Command + +Use this server command in the host's MCP configuration: + +```bash +npx -y gitnexus mcp +``` + +### Useful Tools + +| Tool | Purpose | +|------|---------| +| `gitnexus_query` | Find execution flows and functional areas by concept | +| `gitnexus_context` | Inspect callers, callees, references, and process participation for a symbol | +| `gitnexus_impact` | Understand blast radius before changing a symbol | +| `gitnexus_detect_changes` | Check changed symbols and affected flows before finishing | +| `gitnexus_cypher` | Run direct graph queries | +| `gitnexus_list_repos` | List indexed repositories | + +## ABCoder + +ABCoder parses code into UniAST and gives precise package, file, and node-level structure. Use it for signatures, type shapes, implementations, dependencies, and reverse references. + +### Install + +```bash +go install github.com/cloudwego/abcoder@latest +abcoder --help +``` + +### Parse Repositories + +```bash +abcoder parse /absolute/path/to/package \ + --lang typescript \ + --name package-name \ + --output ~/abcoder-asts +``` + +For monorepos, parse each package with a stable `--name` so task notes can reference the same repository names. + +### MCP Server Command + +Use this server command in the host's MCP configuration: + +```bash +abcoder mcp ~/abcoder-asts +``` + +### Useful Tools + +| Tool | Layer | Purpose | +|------|-------|---------| +| `list_repos` | 1 | List parsed repositories | +| `get_repo_structure` | 2 | Inspect packages and files | +| `get_package_structure` | 3 | Inspect nodes within a package | +| `get_file_structure` | 3 | Inspect functions, classes, types, and signatures in a file | +| `get_ast_node` | 4 | Retrieve code, dependencies, references, and implementations | + +## Verification + +After configuration, verify from the agent host that both MCP servers are visible. Then run one simple query against each server before starting the spec writing pass. + +```bash +ls .gitnexus/meta.json +ls ~/abcoder-asts/*.json +``` diff --git a/.pi/skills/trellis-spec-bootstrap/references/repository-analysis.md b/.pi/skills/trellis-spec-bootstrap/references/repository-analysis.md new file mode 100644 index 0000000..1309d29 --- /dev/null +++ b/.pi/skills/trellis-spec-bootstrap/references/repository-analysis.md @@ -0,0 +1,59 @@ +# Repository Analysis + +The goal is to discover the project's real architecture before writing rules. Do not start from generic spec templates and fill blanks. Start from the code, then let the spec structure follow. + +## Analysis Order + +1. Read the existing `.trellis/spec/` tree and note which files are templates, outdated, or already project-specific. +2. Inspect package manifests, build scripts, workspace config, and top-level documentation to identify packages and runtime layers. +3. Use GitNexus for execution flows, module clusters, dependency hubs, and impact-sensitive areas. +4. Use ABCoder or language-native tooling for exact signatures, types, class boundaries, and implementation examples. +5. Read representative source and test files directly before turning any finding into a spec rule. + +## What To Capture + +| Area | Questions | +|------|-----------| +| Package boundaries | What does each package own? What imports cross boundaries? | +| Runtime layers | Which code is CLI, backend, frontend, worker, shared library, test-only, or tooling? | +| Core abstractions | Which types, services, stores, commands, routes, or adapters define the system shape? | +| Data flow | Where does user input enter, how is it validated, and where does state persist? | +| Error handling | How are failures represented, logged, surfaced, and tested? | +| Configuration | Where do defaults, environment config, generated files, and templates live? | +| Tests | Which test styles are trusted examples for new work? | + +## GitNexus Usage + +Start broad, then inspect specific symbols: + +```text +gitnexus_query({query: "CLI command execution flow"}) +gitnexus_query({query: "template generation and migration"}) +gitnexus_context({name: "SymbolName"}) +gitnexus_cypher({query: "MATCH (n)-[r]->(m) RETURN n.name, type(r), m.name LIMIT 30"}) +``` + +Use GitNexus results to find important files and flows. Do not quote graph output as the final authority until you have checked the relevant source files. + +## ABCoder Usage + +Use ABCoder when the spec needs exact code shapes: + +```text +list_repos() +get_repo_structure({repo_name: "package-name"}) +get_file_structure({repo_name: "package-name", file_path: "src/example.ts"}) +get_ast_node({repo_name: "package-name", node_ids: [{mod_path: "...", pkg_path: "...", name: "SymbolName"}]}) +``` + +ABCoder is most valuable for documenting constructor patterns, function signatures, type contracts, and reference chains. + +## Analysis Notes + +Keep short notes while analyzing. The notes should include: + +- Package or layer name. +- Files that define the local pattern. +- Rules the spec should teach. +- Anti-patterns found in old code, comments, tests, or migration paths. +- Spec files that should be created, deleted, renamed, or merged. diff --git a/.pi/skills/trellis-spec-bootstrap/references/spec-task-planning.md b/.pi/skills/trellis-spec-bootstrap/references/spec-task-planning.md new file mode 100644 index 0000000..dca2687 --- /dev/null +++ b/.pi/skills/trellis-spec-bootstrap/references/spec-task-planning.md @@ -0,0 +1,61 @@ +# Spec Task Planning + +Use a single agent as the default execution model. The agent may create Trellis tasks for traceability, but the skill should not require a specific platform, CLI, or parallel worker model. + +## Decomposition + +Create spec work units around real ownership boundaries: + +- One package when a package has its own conventions. +- One layer when the same package has distinct frontend, backend, CLI, worker, or shared-library rules. +- One cross-cutting guide when a pattern spans packages and is not owned by one layer. + +Avoid artificial decomposition. A small library usually needs one focused spec pass, not several tasks. + +## Task Shape + +When a Trellis task is useful, write a concise PRD with these sections: + +```markdown +# Fill <package-or-layer> Trellis Specs + +## Goal +Write project-specific `.trellis/spec/` guidance for <scope>. + +## Scope +- Spec directory: +- Source directories to inspect: +- Tests to inspect: +- Out of scope: + +## Architecture Context +Summarize the concrete findings from repository analysis. + +## Files To Create Or Update +- `.trellis/spec/.../index.md` +- `.trellis/spec/.../<topic>.md` + +## Rules +- Adapt the spec file set to the real codebase. +- Use real source examples with file paths. +- Remove template-only sections that do not apply. +- Do not modify product source code unless the task explicitly asks for it. + +## Acceptance Criteria +- [ ] Specs contain concrete examples and anti-patterns from the repository. +- [ ] No placeholder text remains. +- [ ] Index files match the final spec files. +- [ ] Claims are backed by source files, tests, or project docs. +``` + +## Optional Helper Agents + +If the host supports subagents, helpers can inspect independent packages or run verification. They are optional. The main agent still owns integration and final quality. + +Helper tasks must have clear ownership: + +- Read-only research tasks may inspect any source needed for the assigned scope. +- Write tasks should own disjoint spec directories. +- Verification tasks should check placeholder removal, broken links, and consistency. + +Do not encode helper-agent names, vendor-specific commands, or platform-specific routing in the skill. Put only the required work and acceptance criteria in the task. diff --git a/.pi/skills/trellis-spec-bootstrap/references/spec-writing.md b/.pi/skills/trellis-spec-bootstrap/references/spec-writing.md new file mode 100644 index 0000000..6bc7dec --- /dev/null +++ b/.pi/skills/trellis-spec-bootstrap/references/spec-writing.md @@ -0,0 +1,70 @@ +# Spec Writing + +Trellis specs are coding guidance for future agents. They should explain how to work in this repository, not how a generic project might be organized. + +## Write From Evidence + +Each important rule should be backed by one of these: + +- A source file that demonstrates the preferred pattern. +- A test file that shows expected behavior. +- A project document that defines the convention. +- A repeated pattern across multiple files. + +Use short snippets only when they make the rule clearer. Prefer linking to the file path and naming the symbol or behavior. + +## File Structure + +Keep the spec tree aligned with the project: + +- Keep `index.md` as the navigation file for the spec directory. +- Split topics when developers would look for them independently. +- Merge topics when separate files would repeat the same rule. +- Delete template files that do not apply. +- Add new files for important local patterns the template missed. + +## Content Standards + +Good spec sections include: + +- When the rule applies. +- The local pattern to follow. +- The source or test files that prove the pattern. +- Common mistakes or anti-patterns. +- Verification commands or checks when they are specific and reliable. + +Avoid: + +- Placeholder prose. +- Generic framework advice. +- Tool instructions that only work in one agent host. +- Long copied code blocks. +- Rules based on a single accidental implementation detail. + +## Example Shape + +```markdown +## Command Handlers + +Command handlers should keep argument parsing, validation, and side effects separate. The local pattern is: + +- Parse CLI flags at the command boundary. +- Convert raw inputs into typed task options before invoking core logic. +- Keep filesystem writes in the command or service layer, not in template helpers. + +Reference files: +- `packages/cli/src/commands/example.ts` +- `packages/cli/test/commands/example.test.ts` + +Avoid passing raw `process.argv` or unvalidated config objects into shared helpers. +``` + +## Final Pass + +Before finishing: + +```bash +grep -R "To be filled\\|TODO: fill\\|placeholder" .trellis/spec +``` + +Also check links, index files, and whether any spec still describes a template rather than this repository. diff --git a/.pi/skills/trellis-update-spec/SKILL.md b/.pi/skills/trellis-update-spec/SKILL.md new file mode 100644 index 0000000..2b828a5 --- /dev/null +++ b/.pi/skills/trellis-update-spec/SKILL.md @@ -0,0 +1,356 @@ +--- +name: trellis-update-spec +description: "Captures executable contracts and coding conventions into .trellis/spec/ documents. Use when learning something valuable from debugging, implementing, or discussion that should be preserved for future sessions." +--- + +# Update Code-Spec - Capture Executable Contracts + +When you learn something valuable (from debugging, implementing, or discussion), use this to update the relevant code-spec documents. + +**Timing**: After completing a task, fixing a bug, or discovering a new pattern + +--- + +## Code-Spec First Rule (CRITICAL) + +In this project, "spec" for implementation work means **code-spec**: +- Executable contracts (not principle-only text) +- Concrete signatures, payload fields, env keys, and boundary behavior +- Testable validation/error behavior + +If the change touches infra or cross-layer contracts, code-spec depth is mandatory. + +### Mandatory Triggers + +Apply code-spec depth when the change includes any of: +- New/changed command or API signature +- Cross-layer request/response contract change +- Database schema/migration change +- Infra integration (storage, queue, cache, secrets, env wiring) + +### Mandatory Output (7 Sections) + +For triggered tasks, include all sections below: +1. Scope / Trigger +2. Signatures (command/API/DB) +3. Contracts (request/response/env) +4. Validation & Error Matrix +5. Good/Base/Bad Cases +6. Tests Required (with assertion points) +7. Wrong vs Correct (at least one pair) + +--- + +## When to Update Code-Specs + +| Trigger | Example | Target Spec | +|---------|---------|-------------| +| **Implemented a feature** | Added a new integration or module | Relevant spec file | +| **Made a design decision** | Chose extensibility pattern over simplicity | Relevant spec + "Design Decisions" section | +| **Fixed a bug** | Found a subtle issue with error handling | Relevant spec (e.g., error-handling docs) | +| **Discovered a pattern** | Found a better way to structure code | Relevant spec file | +| **Hit a gotcha** | Learned that X must be done before Y | Relevant spec + "Common Mistakes" section | +| **Established a convention** | Team agreed on naming pattern | Quality guidelines | +| **New thinking trigger** | "Don't forget to check X before doing Y" | `guides/*.md` (as a checklist item) | + +**Key Insight**: Code-spec updates are NOT just for problems. Every feature implementation contains design decisions and contracts that future AI/developers need to execute safely. + +--- + +## Spec Structure Overview + +``` +.trellis/spec/ +├── <layer>/ # Per-layer coding standards (e.g., backend/, frontend/, api/) +│ ├── index.md # Overview and links +│ └── *.md # Topic-specific guidelines +└── guides/ # Thinking checklists (NOT coding specs!) + ├── index.md # Guide index + └── *.md # Topic-specific guides +``` + +### CRITICAL: Code-Spec vs Guide - Know the Difference + +| Type | Location | Purpose | Content Style | +|------|----------|---------|---------------| +| **Code-Spec** | `<layer>/*.md` | Tell AI "how to implement safely" | Signatures, contracts, matrices, cases, test points | +| **Guide** | `guides/*.md` | Help AI "what to think about" | Checklists, questions, pointers to specs | + +**Decision Rule**: Ask yourself: + +- "This is **how to write** the code" → Put in a spec layer directory +- "This is **what to consider** before writing" → Put in `guides/` + +**Example**: + +| Learning | Wrong Location | Correct Location | +|----------|----------------|------------------| +| "Use API X not API Y for this task" | ❌ `guides/` (too specific for a thinking guide) | ✅ Relevant spec file (concrete convention) | +| "Remember to check X when doing Y" | ❌ Spec file (too abstract for a spec) | ✅ `guides/` (thinking checklist) | + +**Guides should be short checklists that point to specs**, not duplicate the detailed rules. + +--- + +## Update Process + +### Step 1: Identify What You Learned + +Answer these questions: + +1. **What did you learn?** (Be specific) +2. **Why is it important?** (What problem does it prevent?) +3. **Where does it belong?** (Which spec file?) + +### Step 2: Classify the Update Type + +| Type | Description | Action | +|------|-------------|--------| +| **Design Decision** | Why we chose approach X over Y | Add to "Design Decisions" section | +| **Project Convention** | How we do X in this project | Add to relevant section with examples | +| **New Pattern** | A reusable approach discovered | Add to "Patterns" section | +| **Forbidden Pattern** | Something that causes problems | Add to "Anti-patterns" or "Don't" section | +| **Common Mistake** | Easy-to-make error | Add to "Common Mistakes" section | +| **Convention** | Agreed-upon standard | Add to relevant section | +| **Gotcha** | Non-obvious behavior | Add warning callout | + +### Step 3: Read the Target Code-Spec + +Before editing, read the current code-spec to: +- Understand existing structure +- Avoid duplicating content +- Find the right section for your update + +```bash +cat .trellis/spec/<category>/<file>.md +``` + +### Step 4: Make the Update + +Follow these principles: + +1. **Be Specific**: Include concrete examples, not just abstract rules +2. **Explain Why**: State the problem this prevents +3. **Show Contracts**: Add signatures, payload fields, and error behavior +4. **Show Code**: Add code snippets for key patterns +5. **Keep it Short**: One concept per section + +### Step 5: Update the Index (if needed) + +If you added a new section or the code-spec status changed, update the category's `index.md`. + +--- + +## Update Templates + +### Mandatory Template for Infra/Cross-Layer Work + +```markdown +## Scenario: <name> + +### 1. Scope / Trigger +- Trigger: <why this requires code-spec depth> + +### 2. Signatures +- Backend command/API/DB signature(s) + +### 3. Contracts +- Request fields (name, type, constraints) +- Response fields (name, type, constraints) +- Environment keys (required/optional) + +### 4. Validation & Error Matrix +- <condition> -> <error> + +### 5. Good/Base/Bad Cases +- Good: ... +- Base: ... +- Bad: ... + +### 6. Tests Required +- Unit/Integration/E2E with assertion points + +### 7. Wrong vs Correct +#### Wrong +... +#### Correct +... +``` + +### Adding a Design Decision + +```markdown +### Design Decision: [Decision Name] + +**Context**: What problem were we solving? + +**Options Considered**: +1. Option A - brief description +2. Option B - brief description + +**Decision**: We chose Option X because... + +**Example**: +\`\`\`typescript +// How it's implemented +code example +\`\`\` + +**Extensibility**: How to extend this in the future... +``` + +### Adding a Project Convention + +```markdown +### Convention: [Convention Name] + +**What**: Brief description of the convention. + +**Why**: Why we do it this way in this project. + +**Example**: +\`\`\`typescript +// How to follow this convention +code example +\`\`\` + +**Related**: Links to related conventions or specs. +``` + +### Adding a New Pattern + +```markdown +### Pattern Name + +**Problem**: What problem does this solve? + +**Solution**: Brief description of the approach. + +**Example**: +\`\`\` +// Good +code example + +// Bad +code example +\`\`\` + +**Why**: Explanation of why this works better. +``` + +### Adding a Forbidden Pattern + +```markdown +### Don't: Pattern Name + +**Problem**: +\`\`\` +// Don't do this +bad code example +\`\`\` + +**Why it's bad**: Explanation of the issue. + +**Instead**: +\`\`\` +// Do this instead +good code example +\`\`\` +``` + +### Adding a Common Mistake + +```markdown +### Common Mistake: Description + +**Symptom**: What goes wrong + +**Cause**: Why this happens + +**Fix**: How to correct it + +**Prevention**: How to avoid it in the future +``` + +### Adding a Gotcha + +```markdown +> **Warning**: Brief description of the non-obvious behavior. +> +> Details about when this happens and how to handle it. +``` + +--- + +## Interactive Mode + +If you're unsure what to update, answer these prompts: + +1. **What did you just finish?** + - [ ] Fixed a bug + - [ ] Implemented a feature + - [ ] Refactored code + - [ ] Had a discussion about approach + +2. **What did you learn or decide?** + - Design decision (why X over Y) + - Project convention (how we do X) + - Non-obvious behavior (gotcha) + - Better approach (pattern) + +3. **Would future AI/developers need to know this?** + - To understand how the code works → Yes, update spec + - To maintain or extend the feature → Yes, update spec + - To avoid repeating mistakes → Yes, update spec + - Purely one-off implementation detail → Maybe skip + +4. **Which area does it relate to?** + - [ ] Backend code + - [ ] Frontend code + - [ ] Cross-layer data flow + - [ ] Code organization/reuse + - [ ] Quality/testing + +--- + +## Quality Checklist + +Before finishing your code-spec update: + +- [ ] Is the content specific and actionable? +- [ ] Did you include a code example? +- [ ] Did you explain WHY, not just WHAT? +- [ ] Did you include executable signatures/contracts? +- [ ] Did you include validation and error matrix? +- [ ] Did you include Good/Base/Bad cases? +- [ ] Did you include required tests with assertion points? +- [ ] Is it in the right code-spec file? +- [ ] Does it duplicate existing content? +- [ ] Would a new team member understand it? + +--- + +## Relationship to Other Commands + +``` +Development Flow: + Learn something → /trellis-update-spec → Knowledge captured + ↑ ↓ + /trellis-break-loop ←──────────────────── Future sessions benefit + (deep bug analysis) +``` + +- `/trellis-break-loop` - Analyzes bugs deeply, often reveals spec updates needed +- `/trellis-update-spec` - Actually makes the updates +- `/trellis-finish-work` - Reminds you to check if specs need updates + +--- + +## Core Philosophy + +> **Code-specs are living documents. Every debugging session, every "aha moment" is an opportunity to make the implementation contract clearer.** + +The goal is **institutional memory**: +- What one person learns, everyone benefits from +- What AI learns in one session, persists to future sessions +- Mistakes become documented guardrails diff --git a/.spec-workflow/templates/design-template.md b/.spec-workflow/templates/design-template.md new file mode 100644 index 0000000..1295d7b --- /dev/null +++ b/.spec-workflow/templates/design-template.md @@ -0,0 +1,96 @@ +# Design Document + +## Overview + +[High-level description of the feature and its place in the overall system] + +## Steering Document Alignment + +### Technical Standards (tech.md) +[How the design follows documented technical patterns and standards] + +### Project Structure (structure.md) +[How the implementation will follow project organization conventions] + +## Code Reuse Analysis +[What existing code will be leveraged, extended, or integrated with this feature] + +### Existing Components to Leverage +- **[Component/Utility Name]**: [How it will be used] +- **[Service/Helper Name]**: [How it will be extended] + +### Integration Points +- **[Existing System/API]**: [How the new feature will integrate] +- **[Database/Storage]**: [How data will connect to existing schemas] + +## Architecture + +[Describe the overall architecture and design patterns used] + +### Modular Design Principles +- **Single File Responsibility**: Each file should handle one specific concern or domain +- **Component Isolation**: Create small, focused components rather than large monolithic files +- **Service Layer Separation**: Separate data access, business logic, and presentation layers +- **Utility Modularity**: Break utilities into focused, single-purpose modules + +```mermaid +graph TD + A[Component A] --> B[Component B] + B --> C[Component C] +``` + +## Components and Interfaces + +### Component 1 +- **Purpose:** [What this component does] +- **Interfaces:** [Public methods/APIs] +- **Dependencies:** [What it depends on] +- **Reuses:** [Existing components/utilities it builds upon] + +### Component 2 +- **Purpose:** [What this component does] +- **Interfaces:** [Public methods/APIs] +- **Dependencies:** [What it depends on] +- **Reuses:** [Existing components/utilities it builds upon] + +## Data Models + +### Model 1 +``` +[Define the structure of Model1 in your language] +- id: [unique identifier type] +- name: [string/text type] +- [Additional properties as needed] +``` + +### Model 2 +``` +[Define the structure of Model2 in your language] +- id: [unique identifier type] +- [Additional properties as needed] +``` + +## Error Handling + +### Error Scenarios +1. **Scenario 1:** [Description] + - **Handling:** [How to handle] + - **User Impact:** [What user sees] + +2. **Scenario 2:** [Description] + - **Handling:** [How to handle] + - **User Impact:** [What user sees] + +## Testing Strategy + +### Unit Testing +- [Unit testing approach] +- [Key components to test] + +### Integration Testing +- [Integration testing approach] +- [Key flows to test] + +### End-to-End Testing +- [E2E testing approach] +- [User scenarios to test] diff --git a/.spec-workflow/templates/product-template.md b/.spec-workflow/templates/product-template.md new file mode 100644 index 0000000..82e60de --- /dev/null +++ b/.spec-workflow/templates/product-template.md @@ -0,0 +1,51 @@ +# Product Overview + +## Product Purpose +[Describe the core purpose of this product/project. What problem does it solve?] + +## Target Users +[Who are the primary users of this product? What are their needs and pain points?] + +## Key Features +[List the main features that deliver value to users] + +1. **Feature 1**: [Description] +2. **Feature 2**: [Description] +3. **Feature 3**: [Description] + +## Business Objectives +[What are the business goals this product aims to achieve?] + +- [Objective 1] +- [Objective 2] +- [Objective 3] + +## Success Metrics +[How will we measure the success of this product?] + +- [Metric 1]: [Target] +- [Metric 2]: [Target] +- [Metric 3]: [Target] + +## Product Principles +[Core principles that guide product decisions] + +1. **[Principle 1]**: [Explanation] +2. **[Principle 2]**: [Explanation] +3. **[Principle 3]**: [Explanation] + +## Monitoring & Visibility (if applicable) +[How do users track progress and monitor the system?] + +- **Dashboard Type**: [e.g., Web-based, CLI, Desktop app] +- **Real-time Updates**: [e.g., WebSocket, polling, push notifications] +- **Key Metrics Displayed**: [What information is most important to surface] +- **Sharing Capabilities**: [e.g., read-only links, exports, reports] + +## Future Vision +[Where do we see this product evolving in the future?] + +### Potential Enhancements +- **Remote Access**: [e.g., Tunnel features for sharing dashboards with stakeholders] +- **Analytics**: [e.g., Historical trends, performance metrics] +- **Collaboration**: [e.g., Multi-user support, commenting] diff --git a/.spec-workflow/templates/requirements-template.md b/.spec-workflow/templates/requirements-template.md new file mode 100644 index 0000000..1c80ca0 --- /dev/null +++ b/.spec-workflow/templates/requirements-template.md @@ -0,0 +1,50 @@ +# Requirements Document + +## Introduction + +[Provide a brief overview of the feature, its purpose, and its value to users] + +## Alignment with Product Vision + +[Explain how this feature supports the goals outlined in product.md] + +## Requirements + +### Requirement 1 + +**User Story:** As a [role], I want [feature], so that [benefit] + +#### Acceptance Criteria + +1. WHEN [event] THEN [system] SHALL [response] +2. IF [precondition] THEN [system] SHALL [response] +3. WHEN [event] AND [condition] THEN [system] SHALL [response] + +### Requirement 2 + +**User Story:** As a [role], I want [feature], so that [benefit] + +#### Acceptance Criteria + +1. WHEN [event] THEN [system] SHALL [response] +2. IF [precondition] THEN [system] SHALL [response] + +## Non-Functional Requirements + +### Code Architecture and Modularity +- **Single Responsibility Principle**: Each file should have a single, well-defined purpose +- **Modular Design**: Components, utilities, and services should be isolated and reusable +- **Dependency Management**: Minimize interdependencies between modules +- **Clear Interfaces**: Define clean contracts between components and layers + +### Performance +- [Performance requirements] + +### Security +- [Security requirements] + +### Reliability +- [Reliability requirements] + +### Usability +- [Usability requirements] diff --git a/.spec-workflow/templates/structure-template.md b/.spec-workflow/templates/structure-template.md new file mode 100644 index 0000000..1ab1fbc --- /dev/null +++ b/.spec-workflow/templates/structure-template.md @@ -0,0 +1,145 @@ +# Project Structure + +## Directory Organization + +``` +[Define your project's directory structure. Examples below - adapt to your project type] + +Example for a library/package: +project-root/ +├── src/ # Source code +├── tests/ # Test files +├── docs/ # Documentation +├── examples/ # Usage examples +└── [build/dist/out] # Build output + +Example for an application: +project-root/ +├── [src/app/lib] # Main source code +├── [assets/resources] # Static resources +├── [config/settings] # Configuration +├── [scripts/tools] # Build/utility scripts +└── [tests/spec] # Test files + +Common patterns: +- Group by feature/module +- Group by layer (UI, business logic, data) +- Group by type (models, controllers, views) +- Flat structure for simple projects +``` + +## Naming Conventions + +### Files +- **Components/Modules**: [e.g., `PascalCase`, `snake_case`, `kebab-case`] +- **Services/Handlers**: [e.g., `UserService`, `user_service`, `user-service`] +- **Utilities/Helpers**: [e.g., `dateUtils`, `date_utils`, `date-utils`] +- **Tests**: [e.g., `[filename]_test`, `[filename].test`, `[filename]Test`] + +### Code +- **Classes/Types**: [e.g., `PascalCase`, `CamelCase`, `snake_case`] +- **Functions/Methods**: [e.g., `camelCase`, `snake_case`, `PascalCase`] +- **Constants**: [e.g., `UPPER_SNAKE_CASE`, `SCREAMING_CASE`, `PascalCase`] +- **Variables**: [e.g., `camelCase`, `snake_case`, `lowercase`] + +## Import Patterns + +### Import Order +1. External dependencies +2. Internal modules +3. Relative imports +4. Style imports + +### Module/Package Organization +``` +[Describe your project's import/include patterns] +Examples: +- Absolute imports from project root +- Relative imports within modules +- Package/namespace organization +- Dependency management approach +``` + +## Code Structure Patterns + +[Define common patterns for organizing code within files. Below are examples - choose what applies to your project] + +### Module/Class Organization +``` +Example patterns: +1. Imports/includes/dependencies +2. Constants and configuration +3. Type/interface definitions +4. Main implementation +5. Helper/utility functions +6. Exports/public API +``` + +### Function/Method Organization +``` +Example patterns: +- Input validation first +- Core logic in the middle +- Error handling throughout +- Clear return points +``` + +### File Organization Principles +``` +Choose what works for your project: +- One class/module per file +- Related functionality grouped together +- Public API at the top/bottom +- Implementation details hidden +``` + +## Code Organization Principles + +1. **Single Responsibility**: Each file should have one clear purpose +2. **Modularity**: Code should be organized into reusable modules +3. **Testability**: Structure code to be easily testable +4. **Consistency**: Follow patterns established in the codebase + +## Module Boundaries +[Define how different parts of your project interact and maintain separation of concerns] + +Examples of boundary patterns: +- **Core vs Plugins**: Core functionality vs extensible plugins +- **Public API vs Internal**: What's exposed vs implementation details +- **Platform-specific vs Cross-platform**: OS-specific code isolation +- **Stable vs Experimental**: Production code vs experimental features +- **Dependencies direction**: Which modules can depend on which + +## Code Size Guidelines +[Define your project's guidelines for file and function sizes] + +Suggested guidelines: +- **File size**: [Define maximum lines per file] +- **Function/Method size**: [Define maximum lines per function] +- **Class/Module complexity**: [Define complexity limits] +- **Nesting depth**: [Maximum nesting levels] + +## Dashboard/Monitoring Structure (if applicable) +[How dashboard or monitoring components are organized] + +### Example Structure: +``` +src/ +└── dashboard/ # Self-contained dashboard subsystem + ├── server/ # Backend server components + ├── client/ # Frontend assets + ├── shared/ # Shared types/utilities + └── public/ # Static assets +``` + +### Separation of Concerns +- Dashboard isolated from core business logic +- Own CLI entry point for independent operation +- Minimal dependencies on main application +- Can be disabled without affecting core functionality + +## Documentation Standards +- All public APIs must have documentation +- Complex logic should include inline comments +- README files for major modules +- Follow language-specific documentation conventions diff --git a/.spec-workflow/templates/tasks-template.md b/.spec-workflow/templates/tasks-template.md new file mode 100644 index 0000000..be461de --- /dev/null +++ b/.spec-workflow/templates/tasks-template.md @@ -0,0 +1,139 @@ +# Tasks Document + +- [ ] 1. Create core interfaces in src/types/feature.ts + - File: src/types/feature.ts + - Define TypeScript interfaces for feature data structures + - Extend existing base interfaces from base.ts + - Purpose: Establish type safety for feature implementation + - _Leverage: src/types/base.ts_ + - _Requirements: 1.1_ + - _Prompt: Role: TypeScript Developer specializing in type systems and interfaces | Task: Create comprehensive TypeScript interfaces for the feature data structures following requirements 1.1, extending existing base interfaces from src/types/base.ts | Restrictions: Do not modify existing base interfaces, maintain backward compatibility, follow project naming conventions | Success: All interfaces compile without errors, proper inheritance from base types, full type coverage for feature requirements_ + +- [ ] 2. Create base model class in src/models/FeatureModel.ts + - File: src/models/FeatureModel.ts + - Implement base model extending BaseModel class + - Add validation methods using existing validation utilities + - Purpose: Provide data layer foundation for feature + - _Leverage: src/models/BaseModel.ts, src/utils/validation.ts_ + - _Requirements: 2.1_ + - _Prompt: Role: Backend Developer with expertise in Node.js and data modeling | Task: Create a base model class extending BaseModel and implementing validation following requirement 2.1, leveraging existing patterns from src/models/BaseModel.ts and src/utils/validation.ts | Restrictions: Must follow existing model patterns, do not bypass validation utilities, maintain consistent error handling | Success: Model extends BaseModel correctly, validation methods implemented and tested, follows project architecture patterns_ + +- [ ] 3. Add specific model methods to FeatureModel.ts + - File: src/models/FeatureModel.ts (continue from task 2) + - Implement create, update, delete methods + - Add relationship handling for foreign keys + - Purpose: Complete model functionality for CRUD operations + - _Leverage: src/models/BaseModel.ts_ + - _Requirements: 2.2, 2.3_ + - _Prompt: Role: Backend Developer with expertise in ORM and database operations | Task: Implement CRUD methods and relationship handling in FeatureModel.ts following requirements 2.2 and 2.3, extending patterns from src/models/BaseModel.ts | Restrictions: Must maintain transaction integrity, follow existing relationship patterns, do not duplicate base model functionality | Success: All CRUD operations work correctly, relationships are properly handled, database operations are atomic and efficient_ + +- [ ] 4. Create model unit tests in tests/models/FeatureModel.test.ts + - File: tests/models/FeatureModel.test.ts + - Write tests for model validation and CRUD methods + - Use existing test utilities and fixtures + - Purpose: Ensure model reliability and catch regressions + - _Leverage: tests/helpers/testUtils.ts, tests/fixtures/data.ts_ + - _Requirements: 2.1, 2.2_ + - _Prompt: Role: QA Engineer with expertise in unit testing and Jest/Mocha frameworks | Task: Create comprehensive unit tests for FeatureModel validation and CRUD methods covering requirements 2.1 and 2.2, using existing test utilities from tests/helpers/testUtils.ts and fixtures from tests/fixtures/data.ts | Restrictions: Must test both success and failure scenarios, do not test external dependencies directly, maintain test isolation | Success: All model methods are tested with good coverage, edge cases covered, tests run independently and consistently_ + +- [ ] 5. Create service interface in src/services/IFeatureService.ts + - File: src/services/IFeatureService.ts + - Define service contract with method signatures + - Extend base service interface patterns + - Purpose: Establish service layer contract for dependency injection + - _Leverage: src/services/IBaseService.ts_ + - _Requirements: 3.1_ + - _Prompt: Role: Software Architect specializing in service-oriented architecture and TypeScript interfaces | Task: Design service interface contract following requirement 3.1, extending base service patterns from src/services/IBaseService.ts for dependency injection | Restrictions: Must maintain interface segregation principle, do not expose internal implementation details, ensure contract compatibility with DI container | Success: Interface is well-defined with clear method signatures, extends base service appropriately, supports all required service operations_ + +- [ ] 6. Implement feature service in src/services/FeatureService.ts + - File: src/services/FeatureService.ts + - Create concrete service implementation using FeatureModel + - Add error handling with existing error utilities + - Purpose: Provide business logic layer for feature operations + - _Leverage: src/services/BaseService.ts, src/utils/errorHandler.ts, src/models/FeatureModel.ts_ + - _Requirements: 3.2_ + - _Prompt: Role: Backend Developer with expertise in service layer architecture and business logic | Task: Implement concrete FeatureService following requirement 3.2, using FeatureModel and extending BaseService patterns with proper error handling from src/utils/errorHandler.ts | Restrictions: Must implement interface contract exactly, do not bypass model validation, maintain separation of concerns from data layer | Success: Service implements all interface methods correctly, robust error handling implemented, business logic is well-encapsulated and testable_ + +- [ ] 7. Add service dependency injection in src/utils/di.ts + - File: src/utils/di.ts (modify existing) + - Register FeatureService in dependency injection container + - Configure service lifetime and dependencies + - Purpose: Enable service injection throughout application + - _Leverage: existing DI configuration in src/utils/di.ts_ + - _Requirements: 3.1_ + - _Prompt: Role: DevOps Engineer with expertise in dependency injection and IoC containers | Task: Register FeatureService in DI container following requirement 3.1, configuring appropriate lifetime and dependencies using existing patterns from src/utils/di.ts | Restrictions: Must follow existing DI container patterns, do not create circular dependencies, maintain service resolution efficiency | Success: FeatureService is properly registered and resolvable, dependencies are correctly configured, service lifetime is appropriate for use case_ + +- [ ] 8. Create service unit tests in tests/services/FeatureService.test.ts + - File: tests/services/FeatureService.test.ts + - Write tests for service methods with mocked dependencies + - Test error handling scenarios + - Purpose: Ensure service reliability and proper error handling + - _Leverage: tests/helpers/testUtils.ts, tests/mocks/modelMocks.ts_ + - _Requirements: 3.2, 3.3_ + - _Prompt: Role: QA Engineer with expertise in service testing and mocking frameworks | Task: Create comprehensive unit tests for FeatureService methods covering requirements 3.2 and 3.3, using mocked dependencies from tests/mocks/modelMocks.ts and test utilities | Restrictions: Must mock all external dependencies, test business logic in isolation, do not test framework code | Success: All service methods tested with proper mocking, error scenarios covered, tests verify business logic correctness and error handling_ + +- [ ] 4. Create API endpoints + - Design API structure + - _Leverage: src/api/baseApi.ts, src/utils/apiUtils.ts_ + - _Requirements: 4.0_ + - _Prompt: Role: API Architect specializing in RESTful design and Express.js | Task: Design comprehensive API structure following requirement 4.0, leveraging existing patterns from src/api/baseApi.ts and utilities from src/utils/apiUtils.ts | Restrictions: Must follow REST conventions, maintain API versioning compatibility, do not expose internal data structures directly | Success: API structure is well-designed and documented, follows existing patterns, supports all required operations with proper HTTP methods and status codes_ + +- [ ] 4.1 Set up routing and middleware + - Configure application routes + - Add authentication middleware + - Set up error handling middleware + - _Leverage: src/middleware/auth.ts, src/middleware/errorHandler.ts_ + - _Requirements: 4.1_ + - _Prompt: Role: Backend Developer with expertise in Express.js middleware and routing | Task: Configure application routes and middleware following requirement 4.1, integrating authentication from src/middleware/auth.ts and error handling from src/middleware/errorHandler.ts | Restrictions: Must maintain middleware order, do not bypass security middleware, ensure proper error propagation | Success: Routes are properly configured with correct middleware chain, authentication works correctly, errors are handled gracefully throughout the request lifecycle_ + +- [ ] 4.2 Implement CRUD endpoints + - Create API endpoints + - Add request validation + - Write API integration tests + - _Leverage: src/controllers/BaseController.ts, src/utils/validation.ts_ + - _Requirements: 4.2, 4.3_ + - _Prompt: Role: Full-stack Developer with expertise in API development and validation | Task: Implement CRUD endpoints following requirements 4.2 and 4.3, extending BaseController patterns and using validation utilities from src/utils/validation.ts | Restrictions: Must validate all inputs, follow existing controller patterns, ensure proper HTTP status codes and responses | Success: All CRUD operations work correctly, request validation prevents invalid data, integration tests pass and cover all endpoints_ + +- [ ] 5. Add frontend components + - Plan component architecture + - _Leverage: src/components/BaseComponent.tsx, src/styles/theme.ts_ + - _Requirements: 5.0_ + - _Prompt: Role: Frontend Architect with expertise in React component design and architecture | Task: Plan comprehensive component architecture following requirement 5.0, leveraging base patterns from src/components/BaseComponent.tsx and theme system from src/styles/theme.ts | Restrictions: Must follow existing component patterns, maintain design system consistency, ensure component reusability | Success: Architecture is well-planned and documented, components are properly organized, follows existing patterns and theme system_ + +- [ ] 5.1 Create base UI components + - Set up component structure + - Implement reusable components + - Add styling and theming + - _Leverage: src/components/BaseComponent.tsx, src/styles/theme.ts_ + - _Requirements: 5.1_ + - _Prompt: Role: Frontend Developer specializing in React and component architecture | Task: Create reusable UI components following requirement 5.1, extending BaseComponent patterns and using existing theme system from src/styles/theme.ts | Restrictions: Must use existing theme variables, follow component composition patterns, ensure accessibility compliance | Success: Components are reusable and properly themed, follow existing architecture, accessible and responsive_ + +- [ ] 5.2 Implement feature-specific components + - Create feature components + - Add state management + - Connect to API endpoints + - _Leverage: src/hooks/useApi.ts, src/components/BaseComponent.tsx_ + - _Requirements: 5.2, 5.3_ + - _Prompt: Role: React Developer with expertise in state management and API integration | Task: Implement feature-specific components following requirements 5.2 and 5.3, using API hooks from src/hooks/useApi.ts and extending BaseComponent patterns | Restrictions: Must use existing state management patterns, handle loading and error states properly, maintain component performance | Success: Components are fully functional with proper state management, API integration works smoothly, user experience is responsive and intuitive_ + +- [ ] 6. Integration and testing + - Plan integration approach + - _Leverage: src/utils/integrationUtils.ts, tests/helpers/testUtils.ts_ + - _Requirements: 6.0_ + - _Prompt: Role: Integration Engineer with expertise in system integration and testing strategies | Task: Plan comprehensive integration approach following requirement 6.0, leveraging integration utilities from src/utils/integrationUtils.ts and test helpers | Restrictions: Must consider all system components, ensure proper test coverage, maintain integration test reliability | Success: Integration plan is comprehensive and feasible, all system components work together correctly, integration points are well-tested_ + +- [ ] 6.1 Write end-to-end tests + - Set up E2E testing framework + - Write user journey tests + - Add test automation + - _Leverage: tests/helpers/testUtils.ts, tests/fixtures/data.ts_ + - _Requirements: All_ + - _Prompt: Role: QA Automation Engineer with expertise in E2E testing and test frameworks like Cypress or Playwright | Task: Implement comprehensive end-to-end tests covering all requirements, setting up testing framework and user journey tests using test utilities and fixtures | Restrictions: Must test real user workflows, ensure tests are maintainable and reliable, do not test implementation details | Success: E2E tests cover all critical user journeys, tests run reliably in CI/CD pipeline, user experience is validated from end-to-end_ + +- [ ] 6.2 Final integration and cleanup + - Integrate all components + - Fix any integration issues + - Clean up code and documentation + - _Leverage: src/utils/cleanup.ts, docs/templates/_ + - _Requirements: All_ + - _Prompt: Role: Senior Developer with expertise in code quality and system integration | Task: Complete final integration of all components and perform comprehensive cleanup covering all requirements, using cleanup utilities and documentation templates | Restrictions: Must not break existing functionality, ensure code quality standards are met, maintain documentation consistency | Success: All components are fully integrated and working together, code is clean and well-documented, system meets all requirements and quality standards_ diff --git a/.spec-workflow/templates/tech-template.md b/.spec-workflow/templates/tech-template.md new file mode 100644 index 0000000..57cd538 --- /dev/null +++ b/.spec-workflow/templates/tech-template.md @@ -0,0 +1,99 @@ +# Technology Stack + +## Project Type +[Describe what kind of project this is: web application, CLI tool, desktop application, mobile app, library, API service, embedded system, game, etc.] + +## Core Technologies + +### Primary Language(s) +- **Language**: [e.g., Python 3.11, Go 1.21, TypeScript, Rust, C++] +- **Runtime/Compiler**: [if applicable] +- **Language-specific tools**: [package managers, build tools, etc.] + +### Key Dependencies/Libraries +[List the main libraries and frameworks your project depends on] +- **[Library/Framework name]**: [Purpose and version] +- **[Library/Framework name]**: [Purpose and version] + +### Application Architecture +[Describe how your application is structured - this could be MVC, event-driven, plugin-based, client-server, standalone, microservices, monolithic, etc.] + +### Data Storage (if applicable) +- **Primary storage**: [e.g., PostgreSQL, files, in-memory, cloud storage] +- **Caching**: [e.g., Redis, in-memory, disk cache] +- **Data formats**: [e.g., JSON, Protocol Buffers, XML, binary] + +### External Integrations (if applicable) +- **APIs**: [External services you integrate with] +- **Protocols**: [e.g., HTTP/REST, gRPC, WebSocket, TCP/IP] +- **Authentication**: [e.g., OAuth, API keys, certificates] + +### Monitoring & Dashboard Technologies (if applicable) +- **Dashboard Framework**: [e.g., React, Vue, vanilla JS, terminal UI] +- **Real-time Communication**: [e.g., WebSocket, Server-Sent Events, polling] +- **Visualization Libraries**: [e.g., Chart.js, D3, terminal graphs] +- **State Management**: [e.g., Redux, Vuex, file system as source of truth] + +## Development Environment + +### Build & Development Tools +- **Build System**: [e.g., Make, CMake, Gradle, npm scripts, cargo] +- **Package Management**: [e.g., pip, npm, cargo, go mod, apt, brew] +- **Development workflow**: [e.g., hot reload, watch mode, REPL] + +### Code Quality Tools +- **Static Analysis**: [Tools for code quality and correctness] +- **Formatting**: [Code style enforcement tools] +- **Testing Framework**: [Unit, integration, and/or end-to-end testing tools] +- **Documentation**: [Documentation generation tools] + +### Version Control & Collaboration +- **VCS**: [e.g., Git, Mercurial, SVN] +- **Branching Strategy**: [e.g., Git Flow, GitHub Flow, trunk-based] +- **Code Review Process**: [How code reviews are conducted] + +### Dashboard Development (if applicable) +- **Live Reload**: [e.g., Hot module replacement, file watchers] +- **Port Management**: [e.g., Dynamic allocation, configurable ports] +- **Multi-Instance Support**: [e.g., Running multiple dashboards simultaneously] + +## Deployment & Distribution (if applicable) +- **Target Platform(s)**: [Where/how the project runs: cloud, on-premise, desktop, mobile, embedded] +- **Distribution Method**: [How users get your software: download, package manager, app store, SaaS] +- **Installation Requirements**: [Prerequisites, system requirements] +- **Update Mechanism**: [How updates are delivered] + +## Technical Requirements & Constraints + +### Performance Requirements +- [e.g., response time, throughput, memory usage, startup time] +- [Specific benchmarks or targets] + +### Compatibility Requirements +- **Platform Support**: [Operating systems, architectures, versions] +- **Dependency Versions**: [Minimum/maximum versions of dependencies] +- **Standards Compliance**: [Industry standards, protocols, specifications] + +### Security & Compliance +- **Security Requirements**: [Authentication, encryption, data protection] +- **Compliance Standards**: [GDPR, HIPAA, SOC2, etc. if applicable] +- **Threat Model**: [Key security considerations] + +### Scalability & Reliability +- **Expected Load**: [Users, requests, data volume] +- **Availability Requirements**: [Uptime targets, disaster recovery] +- **Growth Projections**: [How the system needs to scale] + +## Technical Decisions & Rationale +[Document key architectural and technology choices] + +### Decision Log +1. **[Technology/Pattern Choice]**: [Why this was chosen, alternatives considered] +2. **[Architecture Decision]**: [Rationale, trade-offs accepted] +3. **[Tool/Library Selection]**: [Reasoning, evaluation criteria] + +## Known Limitations +[Document any technical debt, limitations, or areas for improvement] + +- [Limitation 1]: [Impact and potential future solutions] +- [Limitation 2]: [Why it exists and when it might be addressed] diff --git a/.spec-workflow/user-templates/README.md b/.spec-workflow/user-templates/README.md new file mode 100644 index 0000000..ad36a48 --- /dev/null +++ b/.spec-workflow/user-templates/README.md @@ -0,0 +1,64 @@ +# User Templates + +This directory allows you to create custom templates that override the default Spec Workflow templates. + +## How to Use Custom Templates + +1. **Create your custom template file** in this directory with the exact same name as the default template you want to override: + - `requirements-template.md` - Override requirements document template + - `design-template.md` - Override design document template + - `tasks-template.md` - Override tasks document template + - `product-template.md` - Override product steering template + - `tech-template.md` - Override tech steering template + - `structure-template.md` - Override structure steering template + +2. **Template Loading Priority**: + - The system first checks this `user-templates/` directory + - If a matching template is found here, it will be used + - Otherwise, the default template from `templates/` will be used + +## Example Custom Template + +To create a custom requirements template: + +1. Create a file named `requirements-template.md` in this directory +2. Add your custom structure, for example: + +```markdown +# Requirements Document + +## Executive Summary +[Your custom section] + +## Business Requirements +[Your custom structure] + +## Technical Requirements +[Your custom fields] + +## Custom Sections +[Add any sections specific to your workflow] +``` + +## Template Variables + +Templates can include placeholders that will be replaced when documents are created: +- `{{projectName}}` - The name of your project +- `{{featureName}}` - The name of the feature being specified +- `{{date}}` - The current date +- `{{author}}` - The document author + +## Best Practices + +1. **Start from defaults**: Copy a default template from `../templates/` as a starting point +2. **Keep structure consistent**: Maintain similar section headers for tool compatibility +3. **Document changes**: Add comments explaining why sections were added/modified +4. **Version control**: Track your custom templates in version control +5. **Test thoroughly**: Ensure custom templates work with the spec workflow tools + +## Notes + +- Custom templates are project-specific and not included in the package distribution +- The `templates/` directory contains the default templates which are updated with each version +- Your custom templates in this directory are preserved during updates +- If a custom template has errors, the system will fall back to the default template diff --git a/.trae/agents/trellis-check.md b/.trae/agents/trellis-check.md new file mode 100644 index 0000000..82db8a5 --- /dev/null +++ b/.trae/agents/trellis-check.md @@ -0,0 +1,134 @@ +--- +name: trellis-check +description: | + Code quality check expert. Reviews code changes against specs and self-fixes issues. +tools: Read, Write, Edit, Bash, Glob, Grep +--- + +## 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 `<task-path>/check.jsonl` — JSONL list of spec/research files relevant to this agent. +2. For each entry in the JSONL, Read its `file` path — these are the specs and research notes you must follow. + **Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran). +3. Read the task's `prd.md` (requirements), then `design.md` if present (technical design), then `implement.md` if present (execution plan). + +If `check.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read the task artifacts, 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 — lightweight tasks may be PRD-only, while complex tasks may also include `design.md` and `implement.md`. + +If the resolved task path has no `prd.md`, ask the user what to work on; do NOT proceed without context. + +--- + +# Check Agent + +You are the Check Agent in the Trellis workflow. + +## Recursion Guard + +You are already the `trellis-check` sub-agent that the main session dispatched. Do the review and fixes directly. + +- Do NOT spawn another `trellis-check` or `trellis-implement` sub-agent. +- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. +- Only the main session may dispatch Trellis implement/check agents. If more implementation work is needed, report that recommendation instead of spawning. + +## Context + +Before checking, read: +- `.trellis/spec/` - Development guidelines +- Task `prd.md` - Requirements document +- Task `design.md` - Technical design (if exists) +- Task `implement.md` - Execution plan (if exists) +- Pre-commit checklist for quality standards + +## Core Responsibilities + +1. **Get code changes** - Use git diff to get uncommitted code +2. **Review task artifacts** - Check changes against prd.md, design.md if present, and implement.md if present +3. **Check against specs** - Verify code follows guidelines +4. **Self-fix** - Fix issues yourself, not just report them +5. **Run verification** - typecheck and lint + +## Important + +**Fix issues yourself**, don't just report them. + +You have write and edit tools, you can modify code directly. + +--- + +## Workflow + +### Step 1: Get Changes + +```bash +git diff --name-only # List changed files +git diff # View specific changes +``` + +### Step 2: Check Against Specs and Task Artifacts + +Read the task's prd.md, design.md if present, and implement.md if present, then read relevant specs in `.trellis/spec/` to check code: + +- Does it satisfy the task requirements +- Does it follow the technical design and implementation plan when present +- Does it follow directory structure conventions +- Does it follow naming conventions +- Does it follow code patterns +- Are there missing types +- Are there potential bugs + +### Step 3: Self-Fix + +After finding issues: + +1. Fix the issue directly (use edit tool) +2. Record what was fixed +3. Continue checking other issues + +### Step 4: Run Verification + +Run project's lint and typecheck commands to verify changes. + +If failed, fix issues and re-run. + +--- + +## Report Format + +```markdown +## Self-Check Complete + +### Files Checked + +- src/components/Feature.tsx +- src/hooks/useFeature.ts + +### Issues Found and Fixed + +1. `<file>:<line>` - <what was fixed> +2. `<file>:<line>` - <what was fixed> + +### Issues Not Fixed + +(If there are issues that cannot be self-fixed, list them here with reasons) + +### Verification Results + +- TypeCheck: Passed +- Lint: Passed + +### Summary + +Checked X files, found Y issues, all fixed. +``` diff --git a/.trae/agents/trellis-implement.md b/.trae/agents/trellis-implement.md new file mode 100644 index 0000000..24181a6 --- /dev/null +++ b/.trae/agents/trellis-implement.md @@ -0,0 +1,129 @@ +--- +name: trellis-implement +description: | + Code implementation expert. Understands specs and requirements, then implements features. No git commit allowed. +tools: Read, Write, Edit, Bash, Glob, Grep +--- + +## 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 `<task-path>/implement.jsonl` — JSONL list of spec/research files relevant to this agent. +2. For each entry in the JSONL, Read its `file` path — these are the specs and research notes you must follow. + **Skip rows without a `"file"` field** (e.g. `{"_example": "..."}` seed rows left over from `task.py create` before the curator ran). +3. Read the task's `prd.md` (requirements), then `design.md` if present (technical design), then `implement.md` if present (execution plan). + +If `implement.jsonl` has no curated entries (only a seed row, or the file is missing), fall back to: read the task artifacts, 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 — lightweight tasks may be PRD-only, while complex tasks may also include `design.md` and `implement.md`. + +If the resolved task path has no `prd.md`, ask the user what to work on; do NOT proceed without context. + +--- + +# Implement Agent + +You are the Implement Agent in the Trellis workflow. + +## Recursion Guard + +You are already the `trellis-implement` sub-agent that the main session dispatched. Do the implementation work directly. + +- Do NOT spawn another `trellis-implement` or `trellis-check` sub-agent. +- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. +- Only the main session may dispatch Trellis implement/check agents. If more parallel work is needed, report that recommendation instead of spawning. + +## Context + +Before implementing, read: +- `.trellis/workflow.md` - Project workflow +- `.trellis/spec/` - Development guidelines +- Task `prd.md` - Requirements document +- Task `design.md` - Technical design (if exists) +- Task `implement.md` - Execution plan (if exists) + +## Core Responsibilities + +1. **Understand specs** - Read relevant spec files in `.trellis/spec/` +2. **Understand task artifacts** - Read prd.md, design.md if present, and implement.md if present +3. **Implement features** - Write code following specs and task artifacts +4. **Self-check** - Ensure code quality +5. **Report results** - Report completion status + +## Forbidden Operations + +**Do NOT execute these git commands:** + +- `git commit` +- `git push` +- `git merge` + +--- + +## Workflow + +### 1. Understand Specs + +Read relevant specs based on task type: + +- Spec layers: `.trellis/spec/<package>/<layer>/` +- Shared guides: `.trellis/spec/guides/` + +### 2. Understand Requirements + +Read the task's prd.md, design.md if present, and implement.md if present: + +- What are the core requirements +- Key points of technical design +- Implementation order, validation commands, and rollback points + +### 3. Implement Features + +- Write code following specs and task artifacts +- Follow existing code patterns +- Only do what's required, no over-engineering + +### 4. Verify + +Run project's lint and typecheck commands to verify changes. + +--- + +## Report Format + +```markdown +## Implementation Complete + +### Files Modified + +- `src/components/Feature.tsx` - New component +- `src/hooks/useFeature.ts` - New hook + +### Implementation Summary + +1. Created Feature component... +2. Added useFeature hook... + +### Verification Results + +- Lint: Passed +- TypeCheck: Passed +``` + +--- + +## Code Standards + +- Follow existing code patterns +- Don't add unnecessary abstractions +- Only do what's required, no over-engineering +- Keep code readable diff --git a/.trae/agents/trellis-research.md b/.trae/agents/trellis-research.md new file mode 100644 index 0000000..92608b4 --- /dev/null +++ b/.trae/agents/trellis-research.md @@ -0,0 +1,137 @@ +--- +name: trellis-research +description: | + Code and tech search expert. Finds files, patterns, and tech solutions, and PERSISTS every finding to the current task's research/ directory. No code modifications outside that directory. +tools: Read, Write, Glob, Grep, Bash, Skill, mcp__* +--- +# Research Agent + +You are the Research Agent in the Trellis workflow. + +## Core Principle + +**You do one thing: find, explain, and PERSIST information.** + +Conversations get compacted; files don't. Every research output MUST end up as a file under `{TASK_DIR}/research/`. Returning findings only through the chat reply is a failure — the caller cannot read them next session. + +--- + +## Core Responsibilities + +1. **Internal Search** — locate files/components, understand code logic, discover patterns (Glob, Grep, Read) +2. **External Search** — library docs, API references, best practices (web search) +3. **Persist** — write each research topic to `{TASK_DIR}/research/<topic>.md` +4. **Report** — return file paths + one-line summaries to the main agent (not full content) + +--- + +## Workflow + +### Step 1: Resolve Current Task + +Run `python3 ./.trellis/scripts/task.py current --source` → active task path. If no active task is set, ask the user where to write output; do NOT guess. + +Ensure `{TASK_DIR}/research/` exists: + +```bash +mkdir -p <TASK_DIR>/research +``` + +### Step 2: Understand Search Request + +Classify: internal / external / mixed. Determine scope (global / specific directory) and expected shape (file list / pattern notes / tech comparison). + +### Step 3: Execute Search + +Run independent searches in parallel (Glob + Grep + web) for efficiency. + +### Step 4: Persist Each Topic + +For each distinct research topic, Write a markdown file at `{TASK_DIR}/research/<topic-slug>.md`. Use the File Format below. + +### Step 5: Report to Main Agent + +Reply with ONLY: + +- List of files written (paths relative to repo root) +- One-line summary per file +- Any critical caveats that the main agent needs to know right now + +Do NOT paste full research content into the reply. The files are the contract. + +--- + +## Scope Limits (Strict) + +### Write ALLOWED + +- `{TASK_DIR}/research/*.md` — your own output +- Creating `{TASK_DIR}/research/` if it doesn't exist (via `mkdir -p`) + +### Write FORBIDDEN + +- Code files (`src/`, `lib/`, …) +- Spec files (`.trellis/spec/`) — main agent should use `update-spec` skill instead +- `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.claude/`, `.cursor/`, `.trae/`, etc.) +- Other task directories +- Any git operation (commit / push / branch / merge) + +If the user asks you to edit code, decline and suggest spawning `implement` instead. + +--- + +## File Format + +Each `{TASK_DIR}/research/<topic>.md` should follow: + +```markdown +# Research: <topic> + +- **Query**: <original query> +- **Scope**: <internal / external / mixed> +- **Date**: <YYYY-MM-DD> + +## Findings + +### Files Found + +| File Path | Description | +|---|---| +| `src/services/xxx.ts` | Main implementation | +| `src/types/xxx.ts` | Type definitions | + +### Code Patterns + +<describe patterns, cite file:line> + +### External References + +- [Library X docs](url) — <why relevant, version constraints> + +### Related Specs + +- `.trellis/spec/xxx.md` — <description> + +## Caveats / Not Found + +<anything incomplete or uncertain> +``` + +--- + +## Guidelines + +### DO + +- Provide specific file paths and line numbers +- Quote actual code snippets +- Persist every topic to its own file +- Return file paths in your reply, not the full content +- Mark "not found" explicitly when searches come up empty + +### DON'T + +- Don't write code or modify files outside `{TASK_DIR}/research/` +- Don't guess uncertain info +- Don't paste full research text into the reply (files are the deliverable) +- Don't propose improvements or critique implementation (that's not your role) diff --git a/.trae/commands/trellis-continue.md b/.trae/commands/trellis-continue.md new file mode 100644 index 0000000..1e16d08 --- /dev/null +++ b/.trae/commands/trellis-continue.md @@ -0,0 +1,61 @@ +--- +name: trellis-continue +description: Resume work on the current task at the correct phase. +--- + +# Continue Current Task + +Resume work on the current task — pick up at the right phase/step in `.trellis/workflow.md`. + +--- + +## Step 1: Load Current Context + +```bash +python3 ./.trellis/scripts/get_context.py +``` + +Confirms: current task, git state, recent commits. + +## Step 2: Load the Phase Index + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase +``` + +Shows the Phase Index (Plan / Execute / Finish) with routing + skill mapping. + +## Step 3: Decide Where You Are + +`get_context.py` shows the active task's `status` field. Route by `status` + artifact presence. 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` 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.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 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 + +Once you know which step to resume at: + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase --step <X.X> --platform trae +``` + +Follow the loaded instructions. After each `[required]` step completes, move to the next. + +--- + +## Reference + +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/.trae/commands/trellis-finish-work.md b/.trae/commands/trellis-finish-work.md new file mode 100644 index 0000000..f658f4c --- /dev/null +++ b/.trae/commands/trellis-finish-work.md @@ -0,0 +1,71 @@ +--- +name: trellis-finish-work +description: Wrap up the current session: quality gate, commit reminder, archive, journal. +--- + +# Finish Work + +Wrap up the current session: archive the active task (and any other completed-but-unarchived tasks the user wants to clean up) and record the session journal. Code commits are NOT done here — those happen in workflow Phase 3.4 before you invoke this command. + +## Step 1: Survey current state + +```bash +python3 ./.trellis/scripts/get_context.py --mode record +``` + +This prints: + +- **My active tasks** — review whether any besides the current one are actually done (code merged, AC met) and should be archived this round. +- **Git status** — quick visual on what's dirty. +- **Recent commits** — you'll need their hashes in Step 4 for `--commit`. + +If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless. + +## Step 2: Sanity check — classify dirty paths + +Run: + +```bash +git status --porcelain +``` + +Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this skill's own work. + +For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics: + +- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task +- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task +- Paths in unrelated areas you have no recollection of touching this session → other parallel work + +Then route: + +- **Any remaining path looks like current-task work** — bail out with: + > "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running `/trellis-finish-work`." + + Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there. +- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3: + > "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`." +- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer. + +## Step 3: Archive task(s) + +```bash +python3 ./.trellis/scripts/task.py archive <task-name> +``` + +At minimum: the current active task (if any). Plus any extra tasks the user confirmed in Step 1. Each archive produces a `chore(task): archive ...` commit via the script's auto-commit. + +If there is no active task and the user did not confirm any cleanup archives, skip this step. + +## Step 4: Record session journal + +```bash +python3 ./.trellis/scripts/add_session.py \ + --title "Session Title" \ + --commit "hash1,hash2" \ + --summary "Brief summary" +``` + +Use the work-commit hashes produced in Phase 3.4 (visible in Step 1's `Recent commits` list, or via `git log --oneline`) for `--commit`. Do not include the archive commit hashes from Step 3. This produces a `chore: record journal` commit. + +Final git log order: `<work commits from 3.4>` → `chore(task): archive ...` (one or more) → `chore: record journal`. diff --git a/.trae/hooks.json b/.trae/hooks.json new file mode 100644 index 0000000..6d2a37a --- /dev/null +++ b/.trae/hooks.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 .trae/hooks/session-start.py", + "timeout": 30 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 .trae/hooks/inject-workflow-state.py", + "timeout": 15 + } + ] + } + ] + } +} diff --git a/.trae/hooks/inject-workflow-state.py b/.trae/hooks/inject-workflow-state.py new file mode 100644 index 0000000..e8ab972 --- /dev/null +++ b/.trae/hooks/inject-workflow-state.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +"""Trellis per-turn breadcrumb hook (UserPromptSubmit / BeforeAgent equivalent). + +Runs on every user prompt. Resolves the active task through Trellis' +session-aware active task resolver and emits a short <workflow-state> +block reminding the main AI what task is active and its expected flow. + +The emitted ``hookEventName`` field is platform-aware: most hosts expect +``UserPromptSubmit`` (Claude Code naming, also accepted by Cursor / Qoder / +CodeBuddy / Droid / Codex / Copilot wiring), but Gemini CLI 0.40.x renamed +its per-turn event to ``BeforeAgent`` and its schema validator rejects the +legacy name. ``_detect_platform`` picks the right value at runtime. +Breadcrumb text is pulled exclusively from workflow.md +[workflow-state:STATUS] tag blocks — workflow.md is the single source of +truth. There are no fallback dicts in this script: when workflow.md is +missing or a tag is absent, the breadcrumb degrades to a generic +"Refer to workflow.md for current step." line so users see (and fix) +the broken state instead of the hook silently masking it. + +Shared across all hook-capable platforms (Claude, Cursor, Codex, Qoder, +CodeBuddy, Droid, Gemini, Copilot, Kiro). 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) + - task.json malformed or missing status +""" +from __future__ import annotations + +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 +# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets) +# both in stdin (hook payload from host CLI) and stdout (our emitted blocks) +# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8` +# but applied per-stream so we don't depend on host CLI's command wiring. +if sys.platform.startswith("win"): + import io as _io + for _stream_name in ("stdin", "stdout", "stderr"): + _stream = getattr(sys, _stream_name, None) + if _stream is None: + continue + if hasattr(_stream, "reconfigure"): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] + except Exception: + pass + elif hasattr(_stream, "detach"): + try: + setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) + except Exception: + pass +from typing import Optional + + +# 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> +If you have not already loaded Trellis context this session, read the `trellis-start` skill once. +</trellis-bootstrap>""" + + +# --------------------------------------------------------------------------- +# CWD-robust Trellis root discovery (fixes hook-path-robustness for this hook) +# --------------------------------------------------------------------------- + +def find_trellis_root(start: Path) -> Optional[Path]: + """Walk up from start to find directory containing .trellis/. + + Handles CWD drift: subdirectory launches, monorepo packages, etc. + Returns None if no .trellis/ found (silent no-op). + """ + cur = start.resolve() + while cur != cur.parent: + if (cur / ".trellis").is_dir(): + return cur + cur = cur.parent + return None + + +# --------------------------------------------------------------------------- +# Active task discovery +# --------------------------------------------------------------------------- + +def _detect_platform(input_data: dict) -> str | None: + if isinstance(input_data.get("cursor_version"), str): + return "cursor" + env_map = { + "CLAUDE_PROJECT_DIR": "claude", + "CURSOR_PROJECT_DIR": "cursor", + "CODEBUDDY_PROJECT_DIR": "codebuddy", + "FACTORY_PROJECT_DIR": "droid", + "GEMINI_PROJECT_DIR": "gemini", + "QODER_PROJECT_DIR": "qoder", + "KIRO_PROJECT_DIR": "kiro", + "COPILOT_PROJECT_DIR": "copilot", + "TRAE_PROJECT_DIR": "trae", + } + for env_name, platform in env_map.items(): + if os.environ.get(env_name): + return platform + script_parts = set(Path(sys.argv[0]).parts) + if ".claude" in script_parts: + return "claude" + if ".cursor" in script_parts: + return "cursor" + if ".codex" in script_parts: + return "codex" + if ".gemini" in script_parts: + return "gemini" + if ".qoder" in script_parts: + return "qoder" + if ".codebuddy" in script_parts: + return "codebuddy" + if ".factory" in script_parts: + return "droid" + if ".kiro" in script_parts: + return "kiro" + if ".trae" in script_parts: + return "trae" + return None + + +def _resolve_active_task(root: Path, input_data: dict): + scripts_dir = root / ".trellis" / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_active_task # type: ignore[import-not-found] + + return resolve_active_task(root, input_data, platform=_detect_platform(input_data)) + + +def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, str]]: + """Return (task_id, status, source) from the current active task.""" + active = _resolve_active_task(root, input_data) + if not active.task_path: + return None + + task_dir = Path(active.task_path) + if not task_dir.is_absolute(): + task_dir = root / task_dir + if active.stale: + return task_dir.name, f"stale_{active.source_type}", active.source + + task_json = task_dir / "task.json" + if not task_json.is_file(): + return None + try: + data = json.loads(task_json.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return None + + task_id = data.get("id") or task_dir.name + status = data.get("status", "") + if not isinstance(status, str) or not status: + return None + return task_id, status, active.source + + +# --------------------------------------------------------------------------- +# Breadcrumb loading: parse workflow.md, fall back to hardcoded defaults +# --------------------------------------------------------------------------- + +# Supports STATUS values with letters, digits, underscores, hyphens +# (so "in-review" / "blocked-by-team" work alongside "in_progress"). +_TAG_RE = re.compile( + r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n(.*?)\n\s*\[/workflow-state:\1\]", + re.DOTALL, +) + +def load_breadcrumbs(root: Path) -> dict[str, str]: + """Parse workflow.md for [workflow-state:STATUS] blocks. + + Returns {status: body_text}. workflow.md is the single source of + truth — there are no fallback dicts in this script. Missing tags + (or a missing/unreadable workflow.md) fall back to a generic line + in build_breadcrumb so users see the broken state and fix + workflow.md, rather than the hook silently masking the issue. + """ + workflow = root / ".trellis" / "workflow.md" + if not workflow.is_file(): + return {} + try: + content = workflow.read_text(encoding="utf-8") + except OSError: + return {} + + result: dict[str, str] = {} + for match in _TAG_RE.finditer(content): + status = match.group(1) + body = match.group(2).strip() + if body: + result[status] = body + return result + + +def _read_trellis_config(root: Path) -> dict: + """Load .trellis/config.yaml via the bundled trellis_config helper. + + The helper lives in .trellis/scripts/common; the hook lives outside the + scripts tree, so we extend sys.path before importing. + """ + scripts_dir = root / ".trellis" / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + try: + from common.trellis_config import read_trellis_config # type: ignore[import-not-found] + except Exception: + return {} + try: + return read_trellis_config(root) + except Exception: + return {} + + +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. + """ + 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 + if mode == "sub-agent": + meaning = ( + "sub-agent: implement/check work defaults to Trellis sub-agents; " + "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( + status: str, platform: str | None, config: dict +) -> 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. + + 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 + return f"{status}-inline" if mode == "inline" else status + return status + + +def build_breadcrumb( + task_id: Optional[str], + status: str, + templates: dict[str, str], + source: str | None = None, + breadcrumb_key: str | None = None, +) -> str: + """Build the <workflow-state>...</workflow-state> block. + + - Known status (tag present in workflow.md) → detailed template body + - Unknown status (no tag, or workflow.md missing) → generic + "Refer to workflow.md for current step." line + - `no_task` pseudo-status (task_id is None) → header omits task info + """ + lookup_key = breadcrumb_key or status + body = templates.get(lookup_key) + if body is None and lookup_key != status: + body = templates.get(status) + if body is None: + body = "Refer to workflow.md for current step." + header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})" + return f"<workflow-state>\n{header}\n{body}\n</workflow-state>" + + +# --------------------------------------------------------------------------- +# 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 | BaseException]" = queue.Queue(maxsize=1) + + def _read() -> None: + try: + result_queue.put(sys.stdin.read()) + except BaseException 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, BaseException): + 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 + + data = _load_hook_input() + + cwd_str = data.get("cwd") or os.getcwd() + cwd = Path(cwd_str) + + root = find_trellis_root(cwd) + if root is None: + return 0 # not a Trellis project + + templates = load_breadcrumbs(root) + 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 + # trellis-brainstorm + task.py create when user describes real work. + no_task_key = resolve_breadcrumb_key("no_task", platform, config) + breadcrumb = build_breadcrumb( + None, "no_task", templates, breadcrumb_key=no_task_key + ) + 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_for_breadcrumb, breadcrumb_key=status_key + ) + if platform == "codex": + 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. + hook_event_name = ( + "BeforeAgent" if platform == "gemini" else "UserPromptSubmit" + ) + + output = { + "hookSpecificOutput": { + "hookEventName": hook_event_name, + "additionalContext": breadcrumb, + } + } + print(json.dumps(output)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.trae/hooks/session-start.py b/.trae/hooks/session-start.py new file mode 100644 index 0000000..a7f28bf --- /dev/null +++ b/.trae/hooks/session-start.py @@ -0,0 +1,844 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Session Start Hook - Inject structured context +""" +from __future__ import annotations + +# IMPORTANT: Suppress all warnings FIRST +import warnings +warnings.filterwarnings("ignore") + +import json +import os +import re +import shlex +import subprocess +import sys +from io import StringIO +from pathlib import Path + + +def _normalize_windows_shell_path(path_str: str) -> str: + """Normalize Unix-style shell paths to real Windows paths. + + On Windows, shells like Git Bash / MSYS2 / Cygwin may report paths like + `/d/Users/...` or `/cygdrive/d/Users/...`. `Path.resolve()` will misinterpret + these as `D:/d/Users...` on drive D: (or similar), breaking repo root + detection. + + This function is intentionally conservative: it only rewrites patterns that + unambiguously represent a drive letter mount. + """ + if not isinstance(path_str, str) or not path_str: + return path_str + + # Only relevant on Windows; keep other platforms untouched. + if not sys.platform.startswith("win"): + return path_str + + p = path_str.strip() + + # Already a Windows drive path (C:\... or C:/...) + if re.match(r"^[A-Za-z]:[\/]", p): + return p + + # MSYS/Git-Bash style: /c/Users/... or /d/Work/... + m = re.match(r"^/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + # Cygwin style: /cygdrive/c/Users/... + m = re.match(r"^/cygdrive/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + # WSL mounted drive (sometimes leaked into env): /mnt/c/Users/... + m = re.match(r"^/mnt/([A-Za-z])/(.*)", p) + if m: + drive, rest = m.group(1).upper(), m.group(2) + rest = rest.replace('/', '\\') + return f"{drive}:\\{rest}" + + return path_str + + +FIRST_REPLY_NOTICE = """<first-reply-notice> +First visible reply: say once in Chinese that Trellis SessionStart context is loaded, then answer directly. +This notice is one-shot: do not repeat it after the first assistant reply in the same session. +</first-reply-notice>""" + +# Force UTF-8 on stdin/stdout/stderr on Windows. Default codepage there is +# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets) +# both in stdin (hook payload from host CLI) and stdout (our emitted blocks) +# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8` +# but applied per-stream so we don't depend on host CLI's command wiring. +if sys.platform.startswith("win"): + import io as _io + for _stream_name in ("stdin", "stdout", "stderr"): + _stream = getattr(sys, _stream_name, None) + if _stream is None: + continue + if hasattr(_stream, "reconfigure"): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] + except Exception: + pass + elif hasattr(_stream, "detach"): + try: + setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) + except Exception: + pass + + + +def _has_curated_jsonl_entry(jsonl_path: Path) -> bool: + """Return True iff jsonl has at least one row with a ``file`` field. + + A freshly seeded jsonl only contains a ``{"_example": ...}`` row (no + ``file`` key) — that is NOT "ready". Readiness requires at least one + curated entry. Matches the contract used by hook-inject and pull-based + sub-agent context loaders. + """ + try: + for line in jsonl_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + row = json.loads(line) + except json.JSONDecodeError: + continue + if isinstance(row, dict) and row.get("file"): + return True + except (OSError, UnicodeDecodeError): + return False + return False + + +def should_skip_injection() -> bool: + """Check if any platform's non-interactive flag is set, or if Trellis + hooks are explicitly disabled via TRELLIS_HOOKS=0 / TRELLIS_DISABLE_HOOKS=1. + """ + if os.environ.get("TRELLIS_HOOKS") == "0": + return True + if os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": + return True + non_interactive_vars = [ + "CLAUDE_NON_INTERACTIVE", + "QODER_NON_INTERACTIVE", + "CODEBUDDY_NON_INTERACTIVE", + "FACTORY_NON_INTERACTIVE", + "CURSOR_NON_INTERACTIVE", + "GEMINI_NON_INTERACTIVE", + "KIRO_NON_INTERACTIVE", + "COPILOT_NON_INTERACTIVE", + "TRAE_NON_INTERACTIVE", + ] + return any(os.environ.get(var) == "1" for var in non_interactive_vars) + + +def read_file(path: Path, fallback: str = "") -> str: + try: + return path.read_text(encoding="utf-8") + except (FileNotFoundError, PermissionError): + return fallback + + +def _repo_relative(repo_root: Path, path: Path) -> str: + try: + return path.relative_to(repo_root).as_posix() + except ValueError: + return str(path) + + +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 _detect_platform(input_data: dict) -> str | None: + if isinstance(input_data.get("cursor_version"), str): + return "cursor" + env_map = { + "CLAUDE_PROJECT_DIR": "claude", + "CURSOR_PROJECT_DIR": "cursor", + "CODEBUDDY_PROJECT_DIR": "codebuddy", + "FACTORY_PROJECT_DIR": "droid", + "GEMINI_PROJECT_DIR": "gemini", + "QODER_PROJECT_DIR": "qoder", + "KIRO_PROJECT_DIR": "kiro", + "COPILOT_PROJECT_DIR": "copilot", + "TRAE_PROJECT_DIR": "trae", + } + for env_name, platform in env_map.items(): + if os.environ.get(env_name): + return platform + script_parts = set(Path(sys.argv[0]).parts) + if ".claude" in script_parts: + return "claude" + if ".cursor" in script_parts: + return "cursor" + if ".codex" in script_parts: + return "codex" + if ".gemini" in script_parts: + return "gemini" + if ".qoder" in script_parts: + return "qoder" + if ".codebuddy" in script_parts: + return "codebuddy" + if ".factory" in script_parts: + return "droid" + if ".kiro" in script_parts: + return "kiro" + if ".trae" in script_parts: + return "trae" + return None + + +def _resolve_context_key(trellis_dir: Path, input_data: dict) -> str | None: + scripts_dir = trellis_dir / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_context_key # type: ignore[import-not-found] + + return resolve_context_key(input_data, platform=_detect_platform(input_data)) + + +def _persist_context_key_for_bash(context_key: str | None) -> None: + """Expose Trellis session identity to later Claude Code Bash commands. + + Claude Code SessionStart hooks can append exports to CLAUDE_ENV_FILE; those + variables are then available to Bash tools in the same conversation. Without + this bridge, `task.py start` has hook stdin during SessionStart but no + session identity when the AI later runs it as a normal shell command. + """ + if not context_key: + return + env_file = os.environ.get("CLAUDE_ENV_FILE") + if not env_file: + return + try: + with open(env_file, "a", encoding="utf-8") as handle: + handle.write(f"export TRELLIS_CONTEXT_ID={shlex.quote(context_key)}\n") + except OSError: + pass + + +def _resolve_active_task(trellis_dir: Path, input_data: dict): + scripts_dir = trellis_dir / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from common.active_task import resolve_active_task # type: ignore[import-not-found] + + return resolve_active_task( + trellis_dir.parent, + input_data, + platform=_detect_platform(input_data), + ) + + +def run_script(script_path: Path, context_key: str | None = None) -> str: + try: + if script_path.suffix == ".py": + # Add PYTHONIOENCODING to force UTF-8 in subprocess + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + if context_key: + env["TRELLIS_CONTEXT_ID"] = context_key + cmd = [sys.executable, "-W", "ignore", str(script_path)] + else: + env = os.environ.copy() + if context_key: + env["TRELLIS_CONTEXT_ID"] = context_key + cmd = [str(script_path)] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=5, + cwd=script_path.parent.parent.parent, + env=env, + ) + return result.stdout if result.returncode == 0 else "No context available" + except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError): + return "No context available" + + +def _normalize_task_ref(task_ref: str) -> str: + normalized = task_ref.strip() + if not normalized: + return "" + + path_obj = Path(normalized) + if path_obj.is_absolute(): + return str(path_obj) + + normalized = normalized.replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + + if normalized.startswith("tasks/"): + return f".trellis/{normalized}" + + return normalized + + +def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path: + normalized = _normalize_task_ref(task_ref) + path_obj = Path(normalized) + if path_obj.is_absolute(): + return path_obj + if normalized.startswith(".trellis/"): + return trellis_dir.parent / path_obj + return trellis_dir / "tasks" / path_obj + + +def _get_task_status(trellis_dir: Path, input_data: dict) -> str: + """Return compact active-task status, artifact presence, and next action.""" + active = _resolve_active_task(trellis_dir, input_data) + + if not active.task_path: + return ( + "Status: NO ACTIVE TASK\n" + "Next-Action: Classify the current turn before creating any Trellis task. " + "Simple conversation / small task asks only whether this turn should create a Trellis task. " + "Complex task asks whether task creation and planning are allowed." + ) + + task_ref = active.task_path + task_dir = _resolve_task_dir(trellis_dir, task_ref) + if active.stale or not task_dir.is_dir(): + return ( + f"Status: STALE POINTER\nTask: {task_ref}\n" + f"Next-Action: Run `python3 ./.trellis/scripts/task.py finish` to clear the stale pointer, " + "then ask the user what to work on next." + ) + + task_json_path = task_dir / "task.json" + task_data = {} + if task_json_path.is_file(): + try: + task_data = json.loads(task_json_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, PermissionError): + pass + + task_title = task_data.get("title", task_ref) + task_status = task_data.get("status", "unknown") + artifact_names = ("prd.md", "design.md", "implement.md", "implement.jsonl", "check.jsonl") + present = [name for name in artifact_names if (task_dir / name).is_file()] + if (task_dir / "research").is_dir(): + present.append("research/") + present_line = ", ".join(present) if present else "(none)" + + if task_status == "completed": + return ( + f"Status: COMPLETED\nTask: {task_title}\n" + f"Present: {present_line}\n" + "Next-Action: Run `/trellis:finish-work`. If the working tree is dirty, return to Phase 3.4 first." + ) + + has_prd = (task_dir / "prd.md").is_file() + has_design = (task_dir / "design.md").is_file() + has_implement_plan = (task_dir / "implement.md").is_file() + implement_jsonl = task_dir / "implement.jsonl" + check_jsonl = task_dir / "check.jsonl" + jsonl_ready = ( + (not implement_jsonl.is_file() or _has_curated_jsonl_entry(implement_jsonl)) + and (not check_jsonl.is_file() or _has_curated_jsonl_entry(check_jsonl)) + ) + + if task_status == "planning" and not has_prd: + return ( + f"Status: PLANNING\nTask: {task_title}\n" + f"Present: {present_line}\n" + "Next-Action: Load `trellis-brainstorm` and write `prd.md`. Stay in planning." + ) + + if task_status == "planning": + missing_complex = [ + name for name, exists in ( + ("design.md", has_design), + ("implement.md", has_implement_plan), + ) + if not exists + ] + next_bits: list[str] = [] + if missing_complex: + next_bits.append( + "Lightweight task can request start review with PRD-only; " + f"complex task must add {', '.join(missing_complex)} before start" + ) + else: + next_bits.append("Planning artifacts are present; ask for review before `task.py start`") + if not jsonl_ready: + next_bits.append("curate `implement.jsonl` and `check.jsonl` before sub-agent mode start") + return ( + f"Status: PLANNING\nTask: {task_title}\n" + f"Present: {present_line}\n" + f"Next-Action: {'; '.join(next_bits)}. Do not enter implementation until the user confirms start." + ) + + return ( + f"Status: {str(task_status).upper()}\nTask: {task_title}\n" + f"Present: {present_line}\n" + "Next-Action: Follow the matching per-turn workflow-state. " + "Implementation/check context order is jsonl entries -> `prd.md` -> `design.md if present` -> `implement.md if present`." + ) + + +def _load_trellis_config(trellis_dir: Path, input_data: dict) -> tuple: + """Load Trellis config for session-start decisions. + + Returns: + (is_mono, packages_dict, spec_scope, task_pkg, default_pkg) + """ + scripts_dir = trellis_dir / "scripts" + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + + try: + from common.config import get_default_package, get_packages, get_spec_scope, is_monorepo # type: ignore[import-not-found] + from common.paths import get_current_task # type: ignore[import-not-found] + + repo_root = trellis_dir.parent + is_mono = is_monorepo(repo_root) + packages = get_packages(repo_root) or {} + scope = get_spec_scope(repo_root) + + # Get active task's package + task_pkg = None + current = get_current_task( + repo_root, + input_data, + platform=_detect_platform(input_data), + ) + if current: + task_json = repo_root / current / "task.json" + if task_json.is_file(): + try: + data = json.loads(task_json.read_text(encoding="utf-8")) + if isinstance(data, dict): + tp = data.get("package") + if isinstance(tp, str) and tp: + task_pkg = tp + except (json.JSONDecodeError, OSError): + pass + + default_pkg = get_default_package(repo_root) + return is_mono, packages, scope, task_pkg, default_pkg + except Exception: + return False, {}, None, None, None + + +def _check_legacy_spec(trellis_dir: Path, is_mono: bool, packages: dict) -> str | None: + """Check for legacy spec directory structure in monorepo. + + Returns warning message if legacy structure detected, None otherwise. + """ + if not is_mono or not packages: + return None + + spec_dir = trellis_dir / "spec" + if not spec_dir.is_dir(): + return None + + # Check for legacy flat spec dirs (spec/backend/, spec/frontend/ with index.md) + has_legacy = False + for legacy_name in ("backend", "frontend"): + legacy_dir = spec_dir / legacy_name + if legacy_dir.is_dir() and (legacy_dir / "index.md").is_file(): + has_legacy = True + break + + if not has_legacy: + return None + + # Check which packages are missing spec/<pkg>/ directory + missing = [ + name for name in sorted(packages.keys()) + if not (spec_dir / name).is_dir() + ] + + if not missing: + return None # All packages have spec dirs + + if len(missing) == len(packages): + return ( + f"[!] Legacy spec structure detected: found `spec/backend/` or `spec/frontend/` " + f"but no package-scoped `spec/<package>/` directories.\n" + f"Monorepo packages: {', '.join(sorted(packages.keys()))}\n" + f"Please reorganize: `spec/backend/` -> `spec/<package>/backend/`" + ) + return ( + f"[!] Partial spec migration detected: packages {', '.join(missing)} " + f"still missing `spec/<pkg>/` directory.\n" + f"Please complete migration for all packages." + ) + + +def _resolve_spec_scope( + is_mono: bool, + packages: dict, + scope, + task_pkg: str | None, + default_pkg: str | None, +) -> set | None: + """Resolve which packages should have their specs injected. + + Returns: + Set of package names to include, or None for full scan. + """ + if not is_mono or not packages: + return None # Single-repo: full scan + + if scope is None: + return None # No scope configured: full scan + + if isinstance(scope, str) and scope == "active_task": + if task_pkg and task_pkg in packages: + return {task_pkg} + if default_pkg and default_pkg in packages: + return {default_pkg} + return None # Fallback to full scan + + if isinstance(scope, list): + valid = set() + for entry in scope: + if entry in packages: + valid.add(entry) + else: + print( + f"Warning: spec_scope contains unknown package: {entry}, ignoring", + file=sys.stderr, + ) + + if valid: + # Warn if active task is out of scope + if task_pkg and task_pkg not in valid: + print( + f"Warning: active task package '{task_pkg}' is out of configured spec_scope", + file=sys.stderr, + ) + return valid + + # All entries invalid: fallback chain + print( + "Warning: all spec_scope entries invalid, falling back to task/default/full", + file=sys.stderr, + ) + if task_pkg and task_pkg in packages: + return {task_pkg} + if default_pkg and default_pkg in packages: + return {default_pkg} + return None # Full scan + + return None # Unknown scope type: full scan + + +def _collect_spec_index_paths(trellis_dir: Path, allowed_pkgs: set | None) -> 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 + + if allowed_pkgs is not None and sub.name not in allowed_pkgs: + continue + for nested in sorted(sub.iterdir()): + if not nested.is_dir(): + continue + nested_index = nested / "index.md" + if nested_index.is_file(): + paths.append(f".trellis/spec/{sub.name}/{nested.name}/index.md") + + return paths + + +def _build_compact_current_state( + trellis_dir: Path, + input_data: 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, input_data) + 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 + 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 + + 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`. + + Both parameters are full header lines WITHOUT the `## ` prefix (e.g. "Phase Index"). + Returns empty string if start header is not found. + End header missing → extracts to end of file. + """ + lines = content.splitlines() + start: int | None = None + end: int = len(lines) + start_match = f"## {start_header}" + end_match = f"## {end_header}" + for i, line in enumerate(lines): + stripped = line.strip() + if start is None and stripped == start_match: + start = i + continue + if start is not None and stripped == end_match: + end = i + break + if start is None: + return "" + return "\n".join(lines[start:end]).rstrip() + + +_BREADCRUMB_TAG_RE = re.compile( + r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]", + re.DOTALL, +) + + +def _strip_breadcrumb_tag_blocks(content: str) -> str: + """Remove `[workflow-state:STATUS]...[/workflow-state:STATUS]` blocks. + + The tag blocks live inside `## Phase Index` (since v0.5.0-rc.0, when + they were colocated with their phase summaries) and are consumed by the + UserPromptSubmit hook (`inject-workflow-state.py`). The session-start + payload already covers the full step bodies, so re-inlining the + breadcrumbs here would just duplicate context. + """ + 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_overview(workflow_path: Path) -> str: + """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 - Session Summary", + "Full guide: .trellis/workflow.md. Step detail: `python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y>`.", + "", + ] + + phases = _extract_range(content, "Phase Index", "Phase 1: Plan") + if phases: + out_lines.append(_strip_breadcrumb_tag_blocks(phases).rstrip()) + + return "\n".join(out_lines).rstrip() + + +def main(): + if should_skip_injection(): + sys.exit(0) + + try: + hook_input = json.loads(sys.stdin.read()) + if not isinstance(hook_input, dict): + hook_input = {} + except (json.JSONDecodeError, ValueError): + hook_input = {} + + # Try platform-specific env vars, hook cwd, fallback to cwd + project_dir_env_vars = [ + "CLAUDE_PROJECT_DIR", + "QODER_PROJECT_DIR", + "CODEBUDDY_PROJECT_DIR", + "FACTORY_PROJECT_DIR", + "CURSOR_PROJECT_DIR", + "GEMINI_PROJECT_DIR", + "KIRO_PROJECT_DIR", + "COPILOT_PROJECT_DIR", + "TRAE_PROJECT_DIR", + ] + project_dir = None + for var in project_dir_env_vars: + val = os.environ.get(var) + if val: + project_dir = Path(_normalize_windows_shell_path(val)).resolve() + break + if project_dir is None: + project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve() + + trellis_dir = project_dir / ".trellis" + context_key = _resolve_context_key(trellis_dir, hook_input) + _persist_context_key_for_bash(context_key) + + # Load config for scope filtering and legacy detection + is_mono, packages, scope_config, task_pkg, default_pkg = _load_trellis_config( + trellis_dir, + hook_input, + ) + allowed_pkgs = _resolve_spec_scope(is_mono, packages, scope_config, task_pkg, default_pkg) + + output = StringIO() + + spec_index_paths = _collect_spec_index_paths(trellis_dir, allowed_pkgs) + + output.write("""<session-context> +Trellis compact SessionStart context. Use it to orient the session; load details on demand. +</session-context> + +""") + output.write(FIRST_REPLY_NOTICE) + output.write("\n\n") + + # Legacy migration warning + legacy_warning = _check_legacy_spec(trellis_dir, is_mono, packages) + if legacy_warning: + output.write(f"<migration-warning>\n{legacy_warning}\n</migration-warning>\n\n") + + output.write("<current-state>\n") + output.write(_build_compact_current_state(trellis_dir, hook_input, spec_index_paths)) + output.write("\n</current-state>\n\n") + + output.write("<trellis-workflow>\n") + output.write(_build_workflow_overview(trellis_dir / "workflow.md")) + output.write("\n</trellis-workflow>\n\n") + + output.write("<guidelines>\n") + output.write( + "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" + ) + + 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") + + output.write( + "Discover more via: " + "`python3 ./.trellis/scripts/get_context.py --mode packages`\n" + ) + output.write("</guidelines>\n\n") + + # Check task status and inject structured tag + task_status = _get_task_status(trellis_dir, hook_input) + output.write(f"<task-status>\n{task_status}\n</task-status>\n\n") + + output.write("""<ready> +Context loaded. Follow <task-status>. Load workflow/spec/task details only when needed. +</ready>""") + + context_text = output.getvalue() + + # Kiro (CLI trellis agent agentSpawn) adds a hook's stdout directly to the + # conversation context — no JSON envelope. Emit the bare overview text. + # Conditionally isolated: all other platforms keep the JSON path below. + if _detect_platform(hook_input) == "kiro": + print(context_text, flush=True) + return + + result = { + # Claude Code / Qoder / CodeBuddy / Droid / Gemini / Copilot format + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": context_text, + }, + # Cursor sessionStart format (top-level snake_case per Cursor docs) + "additional_context": context_text, + } + + # Output JSON - stdout is already configured for UTF-8 + print(json.dumps(result, ensure_ascii=False), flush=True) + + +if __name__ == "__main__": + main() diff --git a/.trae/skills/trellis-before-dev/SKILL.md b/.trae/skills/trellis-before-dev/SKILL.md new file mode 100644 index 0000000..5a4b852 --- /dev/null +++ b/.trae/skills/trellis-before-dev/SKILL.md @@ -0,0 +1,40 @@ +--- +name: trellis-before-dev +description: "Discovers and injects project-specific coding guidelines from .trellis/spec/ before implementation begins. Reads spec indexes, pre-development checklists, and shared thinking guides for the target package. Use when starting a new coding task, before writing any code, switching to a different package, or needing to refresh project conventions and standards." +--- + +Read the relevant development guidelines before starting your task. + +Execute these steps: + +1. **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 + ``` + +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 + +4. **Read the spec index** for each relevant module: + ```bash + cat .trellis/spec/<package>/<layer>/index.md + ``` + Follow the **"Pre-Development Checklist"** section in the index. + +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. + +6. **Always read shared guides**: + ```bash + cat .trellis/spec/guides/index.md + ``` + +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/.trae/skills/trellis-brainstorm/SKILL.md b/.trae/skills/trellis-brainstorm/SKILL.md new file mode 100644 index 0000000..806ee31 --- /dev/null +++ b/.trae/skills/trellis-brainstorm/SKILL.md @@ -0,0 +1,173 @@ +--- +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." +--- + +# Trellis Brainstorm + +## Non-Negotiable Interview Contract + +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. + +Ask the questions one at a time. + +## 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. + +Do not ask the user to confirm facts that the repository can answer. Ask only for product intent, preference, scope, risk tolerance, or decisions that remain ambiguous after inspection. + +--- + +Use this skill during Phase 1 planning to turn the user's request into clear requirements and planning artifacts. + +## Preconditions + +Use this skill only after task-creation consent has been given and the user is ready to enter Trellis planning. + +If no task exists yet, create one: + +```bash +TASK_DIR=$(python3 ./.trellis/scripts/task.py create "<short task title>" --slug <slug>) +``` + +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. + +`task.py create` creates the default `prd.md`. Update that file with the current understanding before asking follow-up questions. + +## Planning Flow + +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. Ask the single highest-value remaining question. +5. Include your recommended answer with the question. +6. After each user answer, update `prd.md` before continuing. +7. For complex tasks, create or update `design.md` and `implement.md` before implementation starts. +8. Before final review or `task.py start`, run the PRD convergence pass below. + +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. + +## Question Rules + +Ask only one question per message. + +Each question must include: + +- the decision needed +- why the answer matters +- your recommended answer +- the trade-off if the user chooses differently + +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. + +## 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. + +### Step 1: Restate the Problem + +Strip away implementation details to one sentence. + +> 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 + +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 + +For each component of the current plan: + +- **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. + +### Step 4: Build Up from Truths + +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?" + +### Step 5: Validate + +- Does the solution solve the original problem? +- What assumptions need verification? +- What's the simplest experiment to test this? + +## Artifact Rules + +`prd.md` records requirements and acceptance: + +- goal and user value +- confirmed facts +- requirements +- acceptance criteria +- out of scope +- open questions that still block planning + +`design.md` records technical design for complex tasks: + +- architecture and boundaries +- data flow and contracts +- compatibility and migration notes +- important trade-offs +- operational or rollback considerations + +`implement.md` records execution planning for complex tasks: + +- ordered implementation checklist +- validation commands +- risky files or rollback points +- follow-up checks before `task.py start` + +Lightweight tasks may have only `prd.md`. Complex tasks must have `prd.md`, `design.md`, and `implement.md` before `task.py start`. + +`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`. + +## PRD Convergence Pass + +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. + +The pass must be lossless: + +- 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. +- Keep only genuinely blocking open questions. + +After the pass, read `prd.md` top to bottom and verify that no fact is repeated across sections unless the repetition adds new information. + +## Quality Bar + +Before declaring planning ready: + +- `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. +- Remaining open questions are genuinely about user intent or scope. +- 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 user has reviewed the final planning artifacts or explicitly approved proceeding. + +Do not start implementation until the user approves or asks for implementation. diff --git a/.trae/skills/trellis-break-loop/SKILL.md b/.trae/skills/trellis-break-loop/SKILL.md new file mode 100644 index 0000000..1c8b397 --- /dev/null +++ b/.trae/skills/trellis-break-loop/SKILL.md @@ -0,0 +1,188 @@ +--- +name: trellis-break-loop +description: "Deep bug analysis to break the fix-forget-repeat cycle. Analyzes root cause category, why fixes failed, prevention mechanisms, and captures knowledge into specs. Use after fixing a bug to prevent the same class of bugs." +--- + +# Break the Loop - Deep Bug Analysis + +When debug is complete, use this for deep analysis to break the "fix bug -> forget -> repeat" cycle. + +--- + +## Analysis Framework + +Analyze the bug you just fixed from these 5 dimensions: + +### 1. Root Cause Category + +Which category does this bug belong to? + +| Category | Characteristics | Example | +|----------|-----------------|---------| +| **A. Missing Spec** | No documentation on how to do it | New feature without checklist | +| **B. Cross-Layer Contract** | Interface between layers unclear | API returns different format than expected | +| **C. Change Propagation Failure** | Changed one place, missed others | Changed function signature, missed call sites | +| **D. Test Coverage Gap** | Unit test passes, integration fails | Works alone, breaks when combined | +| **E. Implicit Assumption** | Code relies on undocumented assumption | Timestamp seconds vs milliseconds | + +### 2. Why Fixes Failed (if applicable) + +If you tried multiple fixes before succeeding, analyze each failure: + +- **Surface Fix**: Fixed symptom, not root cause +- **Incomplete Scope**: Found root cause, didn't cover all cases +- **Tool Limitation**: grep missed it, type check wasn't strict +- **Mental Model**: Kept looking in same layer, didn't think cross-layer + +### 3. Prevention Mechanisms + +What mechanisms would prevent this from happening again? + +| Type | Description | Example | +|------|-------------|---------| +| **Documentation** | Write it down so people know | Update thinking guide | +| **Architecture** | Make the error impossible structurally | Type-safe wrappers | +| **Compile-time** | Strict type checking, no escape hatches | Signature change causes compile error | +| **Runtime** | Monitoring, alerts, scans | Detect orphan entities | +| **Test Coverage** | E2E tests, integration tests | Verify full flow | +| **Code Review** | Checklist, PR template | "Did you check X?" | + +### 4. Systematic Expansion + +What broader problems does this bug reveal? + +- **Similar Issues**: Where else might this problem exist? +- **Design Flaw**: Is there a fundamental architecture issue? +- **Process Flaw**: Is there a development process improvement? +- **Knowledge Gap**: Is the team missing some understanding? + +### 5. Knowledge Capture + +Solidify insights into the system: + +- [ ] Update `.trellis/spec/guides/` thinking guides +- [ ] Update relevant `.trellis/spec/` docs +- [ ] Create issue record (if applicable) +- [ ] Create feature ticket for root fix +- [ ] Update check guidelines if needed + +--- + +## Output Format + +Please output analysis in this format: + +```markdown +## Bug Analysis: [Short Description] + +### 1. Root Cause Category +- **Category**: [A/B/C/D/E] - [Category Name] +- **Specific Cause**: [Detailed description] + +### 2. Why Fixes Failed (if applicable) +1. [First attempt]: [Why it failed] +2. [Second attempt]: [Why it failed] +... + +### 3. Prevention Mechanisms +| Priority | Mechanism | Specific Action | Status | +|----------|-----------|-----------------|--------| +| P0 | ... | ... | TODO/DONE | + +### 4. Systematic Expansion +- **Similar Issues**: [List places with similar problems] +- **Design Improvement**: [Architecture-level suggestions] +- **Process Improvement**: [Development process suggestions] + +### 5. Knowledge Capture +- [ ] [Documents to update / tickets to create] +``` + +--- + +## Core Philosophy + +> **The value of debugging is not in fixing the bug, but in making this class of bugs never happen again.** + +Three levels of insight: +1. **Tactical**: How to fix THIS bug +2. **Strategic**: How to prevent THIS CLASS of bugs +3. **Philosophical**: How to expand thinking patterns + +30 minutes of analysis saves 30 hours of future debugging. + +## 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 + +**IMPORTANT**: After completing the analysis above, you MUST immediately: + +1. **Update spec/guides** - Don't just list TODOs, actually update the relevant files: + - If it's a cross-platform issue → update `cross-platform-thinking-guide.md` + - If it's a cross-layer issue → update `cross-layer-thinking-guide.md` + - If it's a code reuse issue → update `code-reuse-thinking-guide.md` + - If it's domain-specific → update `backend/*.md` or `frontend/*.md` + +2. **Sync templates** - After updating `.trellis/spec/`, sync to `src/templates/markdown/spec/` + +3. **Commit the spec updates** - This is the primary output, not just the analysis text + +> **The analysis is worthless if it stays in chat. The value is in the updated specs.** diff --git a/.trae/skills/trellis-channel/SKILL.md b/.trae/skills/trellis-channel/SKILL.md new file mode 100644 index 0000000..511ee02 --- /dev/null +++ b/.trae/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 <board> --scope global +trellis channel thread <board> <thread> --scope global +trellis channel context list <board> --scope global --thread <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 <my_signal>` 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/.trae/skills/trellis-channel/references/command-reference.md b/.trae/skills/trellis-channel/references/command-reference.md new file mode 100644 index 0000000..75def26 --- /dev/null +++ b/.trae/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 <project|global>` unless noted; `project` +is the default and resolves against the current cwd's project bucket. + +## Top-level + +``` +trellis channel <subcommand> +``` + +> Multi-agent collaboration runtime — spawn / coordinate / interrupt worker +> agents through a shared event log. + +--- + +## Create / List + +### `create <name>` + +```bash +trellis channel create <name> + [--scope project|global] # default: project + [--type chat|forum] # default: chat + [--task <path>] # associated Trellis task dir + [--project <slug>] + [--labels a,b,c] + [--description <text>] # stable channel description + [--context-file <abs-path>] ... # repeatable + [--context-raw <text>] ... # repeatable + [--linked-context-file <abs-path>] # [deprecated alias] + [--linked-context-raw <text>] # [deprecated alias] + [--cwd <path>] # recorded in create event + [--by <agent>] # 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 <slug>] # 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 <name> [text]` + +```bash +trellis channel send <name> [text] + --as <agent> # REQUIRED — author + [--scope project|global] + [--to <agents,csv>] # default: broadcast + [--stdin | --text-file <path>] # 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 <name>` + +```bash +trellis channel messages <name> + [--scope project|global] + [--raw] # one JSON event per line + [--follow] # stream new events + [--last <N>] # last N matching events + [--since <seq>] # seq > N + [--kind <kind>] # one of CHANNEL_EVENT_KINDS + [--from <csv>] # author filter + [--to <target>] # routing target filter + [--thread <key>] # forum-only + [--action <thread-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 <name>` + +```bash +trellis channel wait <name> + --as <agent> # REQUIRED — self for filter ctx + [--scope project|global] + [--timeout <Ns|Nm|Nh|Nms>] # parsed by parseDuration + [--from <a,b>] # author CSV + [--kind <k1,k2>] # CSV, OR semantics + [--thread <key>] # forum filter + [--action <thread-action>] # forum filter + [--to <target>] # 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 '<x>'. 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 <name> [text]` + +```bash +trellis channel interrupt <name> [text] + --as <agent> # REQUIRED — caller + --to <agent> # REQUIRED — target worker + [--scope project|global] + [--stdin | --text-file <path>] +``` + +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 <name>` + +```bash +trellis channel spawn <name> + [--scope project|global] + [--agent <agent-name>] # loads .trellis/agents/<name>.md + [--provider claude|codex] # overrides agent file + [--as <worker-name>] # default: agent name + [--cwd <path>] + [--model <id>] + [--resume <id>] # session/thread id resume + [--timeout <Ns|Nm|Nh>] # auto-kill after duration + [--warn-before <Ns|Nm|Nh>] # supervisor_warning lead time + # default 5m, 0ms disables + [--file <path>] ... # glob, repeatable; inject content + [--jsonl <path>] ... # Trellis manifest, repeatable + [--by <agent>] # spawn-event author + # default: TRELLIS_CHANNEL_AS env or 'main' + [--inbox-policy explicitOnly|broadcastAndExplicit] + # default explicitOnly + [--idle-timeout <Ns|Nm|Nh>] # OOM-guard idle TTL + # default 5m, 0 disables + [--max-live-workers <n>] # 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 <worker>`. +- 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 <name>] + [--provider claude|codex] + [--as <worker-name>] + [--cwd <path>] + [--model <id>] + [--file <path>] ... # repeatable, glob + [--jsonl <path>] ... # repeatable + [--message <text> | --message-file <path> | --stdin] + [--timeout <Ns|Nm|Nh>] # default 5m +``` + +Behavior: +- One-shot. Auto-generates `run-<hex>` 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 <name>` + +```bash +trellis channel kill <name> + --as <agent> # 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 <name>` + +```bash +trellis channel rm <name> + [--scope project|global] +``` + +Behavior: +- Kills any live workers, then deletes the entire channel directory. +- Prints `Removed channel '<name>'`. + +### `prune` + +```bash +trellis channel prune + [--scope project|global] # omitted: scan every project + [--all | --empty | --idle <Ns|Nm|Nh|Nd> | --ephemeral] # mutually exclusive + [--yes] # actually delete (default: dry-run) + [--dry-run] # default true; redundant with default + [--keep <names,csv>] # 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 <name> <action>` + +```bash +trellis channel post <name> <action> + --as <agent> # REQUIRED + [--scope project|global] + [--thread <key>] # required except action=opened + [--title <text>] + [--text <text> | --stdin | --text-file <path>] + [--description <text>] # stable thread description + [--status <status>] + [--labels a,b] # REPLACES thread labels + [--assignees a,b] # REPLACES assignees + [--summary <text>] + [--context-file <abs-path>] ... + [--context-raw <text>] ... + [--linked-context-file <abs-path>] # [deprecated alias] + [--linked-context-raw <text>] # [deprecated alias] +``` + +Behavior: +- `<action>` 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 <name>` + +```bash +trellis channel forum <name> + [--scope project|global] + [--status <status>] + [--raw] +``` + +Behavior: +- Lists threads (reduced state). `--status` filters by current thread + status. `--raw` prints one JSON per thread. + +### `thread <name> <thread>` / `thread rename` + +```bash +trellis channel thread <name> <thread-key> + [--scope project|global] + [--raw] + +trellis channel thread rename <name> <old-thread> <new-thread> + --as <agent> # REQUIRED + [--scope project|global] +``` + +Behavior: +- `thread <name> <key>` shows one thread's timeline: + header `<thread> [<status>] <title>`, 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/.trae/skills/trellis-channel/references/forum.md b/.trae/skills/trellis-channel/references/forum.md new file mode 100644 index 0000000..06b7f36 --- /dev/null +++ b/.trae/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/.trae/skills/trellis-channel/references/progress-debugging.md b/.trae/skills/trellis-channel/references/progress-debugging.md new file mode 100644 index 0000000..3ed40d6 --- /dev/null +++ b/.trae/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/.trae/skills/trellis-channel/references/workers.md b/.trae/skills/trellis-channel/references/workers.md new file mode 100644 index 0000000..bcec98f --- /dev/null +++ b/.trae/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/.trae/skills/trellis-channel/references/workflows.md b/.trae/skills/trellis-channel/references/workflows.md new file mode 100644 index 0000000..3319764 --- /dev/null +++ b/.trae/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/.trae/skills/trellis-check/SKILL.md b/.trae/skills/trellis-check/SKILL.md new file mode 100644 index 0000000..c695abd --- /dev/null +++ b/.trae/skills/trellis-check/SKILL.md @@ -0,0 +1,98 @@ +--- +name: trellis-check +description: "Comprehensive quality verification: spec compliance, lint, type-check, tests, cross-layer data flow, code reuse, and consistency checks. Use when code is written and needs quality verification, before committing changes, or to catch context drift during long sessions." +--- + +# Code Quality Check + +Comprehensive quality verification for recently written code. Combines spec compliance, cross-layer safety, and pre-commit checks. + +--- + +## Step 1: Identify What Changed + +```bash +git diff --name-only HEAD +git status +``` + +## Step 2: Read 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 +``` + +For each changed package/layer, read the spec index and follow its **Quality Check** section: + +```bash +cat .trellis/spec/<package>/<layer>/index.md +``` + +Read the specific guideline files referenced — the index is a pointer, not the goal. + +## Step 3: Run Project Checks + +Run the project's lint, type-check, and test commands. Fix any failures before proceeding. + +## Step 4: Review Against Checklist + +### Code Quality + +- [ ] Linter passes? +- [ ] Type checker passes (if applicable)? +- [ ] Tests pass? +- [ ] No debug logging left in? +- [ ] No suppressed warnings or type-safety bypasses? + +### Test Coverage + +- [ ] New function → unit test added? +- [ ] Bug fix → regression test added? +- [ ] Changed behavior → existing tests updated? + +### Spec Sync + +- [ ] Does `.trellis/spec/` need updates? (new patterns, conventions, lessons learned) + +> "If I fixed a bug or discovered something non-obvious, should I document it so future me won't hit the same issue?" → If YES, update the relevant spec doc. + +## Step 5: Cross-Layer Dimensions (if applicable) + +Skip this step if your change is confined to a single layer. + +### A. Data Flow (changes touch 3+ layers) + +- [ ] Read flow traces correctly: Storage → Service → API → UI +- [ ] Write flow traces correctly: UI → API → Service → Storage +- [ ] Types/schemas correctly passed between layers? +- [ ] Errors properly propagated to caller? + +### B. Code Reuse (modifying constants, creating utilities) + +- [ ] Searched for existing similar code before creating new? + ```bash + grep -r "pattern" src/ + ``` +- [ ] If 2+ places define same value → extracted to shared constant? +- [ ] After batch modification, all occurrences updated? + +### C. Import/Dependency (creating new files) + +- [ ] Correct import paths (relative vs absolute)? +- [ ] No circular dependencies? + +### D. Same-Layer Consistency + +- [ ] Other places using the same concept are consistent? + +--- + +## Step 6: Report and Fix + +Report violations found and fix them directly. Re-run project checks after fixes. diff --git a/.trae/skills/trellis-meta/SKILL.md b/.trae/skills/trellis-meta/SKILL.md new file mode 100644 index 0000000..0754b53 --- /dev/null +++ b/.trae/skills/trellis-meta/SKILL.md @@ -0,0 +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, 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, bundled runtime agents, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.reasonix/`, `.kilocode/`, `.agent/`, `.devin/`, 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. +- 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` — 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 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 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 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 15 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, 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; 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` 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/.trae/skills/trellis-meta/references/customize-local/add-project-local-conventions.md b/.trae/skills/trellis-meta/references/customize-local/add-project-local-conventions.md new file mode 100644 index 0000000..608aaa6 --- /dev/null +++ b/.trae/skills/trellis-meta/references/customize-local/add-project-local-conventions.md @@ -0,0 +1,83 @@ +# Add Project-Local Conventions + +Often the user does not need to change Trellis mechanics; they need local AI to understand their team's conventions. In that case, prefer `.trellis/spec/` or a project-local skill instead of editing `trellis-meta`. + +## Where To Put Things + +| Content type | Location | +| --- | --- | +| Rules code must follow | `.trellis/spec/<layer>/` | +| Cross-layer thinking methods | `.trellis/spec/guides/` | +| AI capability for a project-specific flow | Platform-local skill | +| One-off task material | `.trellis/tasks/<task>/` | +| Session summary | `.trellis/workspace/<developer>/journal-N.md` | + +## Create A Project-Local Skill + +If the user wants AI to know "how this project customizes Trellis," create a local skill: + +```text +.claude/skills/trellis-local/ +└── SKILL.md +``` + +Example: + +```md +--- +name: trellis-local +description: "Project-local Trellis customizations for this repository. Use when changing this project's Trellis workflow, hooks, local agents, or team-specific conventions." +--- + +# Trellis Local + +## Local Scope + +This skill documents this repository's Trellis customizations only. + +## Custom Workflow Rules + +- ... + +## Local Hook Changes + +- ... + +## Local Agent Changes + +- ... +``` + +For multi-platform projects, place equivalent versions in other platform skill directories, or use `.agents/skills/` for platforms that support the shared layer. + +## Write To `.trellis/spec/` + +If the content is a coding convention, write it to spec. Examples: + +```text +.trellis/spec/backend/error-handling.md +.trellis/spec/frontend/components.md +.trellis/spec/guides/cross-platform-thinking-guide.md +``` + +After writing it, update the corresponding `index.md` so AI can find the new rule from the entry point. + +## Make The Current Task Use New Conventions + +After writing a spec, add it to the current task context: + +```bash +python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/backend/error-handling.md" "Error handling conventions" +python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/backend/error-handling.md" "Review error handling" +``` + +## Do Not Store Project-Private Rules In `trellis-meta` + +`trellis-meta` is a public skill for understanding Trellis architecture and local customization entry points. Put project-private content in: + +- `.trellis/spec/` +- a project-local skill +- the current task +- workspace journal + +This prevents future updates to Trellis's built-in `trellis-meta` from overwriting the team's own conventions. diff --git a/.trae/skills/trellis-meta/references/customize-local/change-agents.md b/.trae/skills/trellis-meta/references/customize-local/change-agents.md new file mode 100644 index 0000000..88e7376 --- /dev/null +++ b/.trae/skills/trellis-meta/references/customize-local/change-agents.md @@ -0,0 +1,56 @@ +# Change Local Agents + +When the user wants to change `trellis-research`, `trellis-implement`, or `trellis-check` behavior, edit platform agent files in the user project. + +## Read These Files First + +1. Target platform agent directory +2. `.trellis/workflow.md` Phase 2 / research routing +3. Current task `prd.md` +4. Current task `implement.jsonl` / `check.jsonl` +5. Relevant hook or agent prelude + +## Common Paths + +| Platform | Path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/cli/agents/trellis-*.md` | + +Use the actual paths in the user project as authoritative. + +## Common Needs + +| Need | Which agent to edit | +| --- | --- | +| Research must write files, not only reply in chat | `trellis-research` | +| Certain local specs must be read before implementation | `trellis-implement` + `implement.jsonl` configuration rules | +| Specific commands must run during checking | `trellis-check` | +| Agent must not modify certain directories | The corresponding agent's write boundary instructions | +| Agent output format must be fixed | The corresponding agent's final/reporting instructions | + +## Modification Principles + +1. **Preserve role boundaries**: research investigates and persists; implement writes implementation; check reviews and fixes. +2. **Do not hard-code project specs into agents**: long-term specs belong in `.trellis/spec/`; agents are responsible for reading them. +3. **Make read order explicit**: active task -> PRD -> info -> JSONL -> spec/research. +4. **Make write boundaries explicit**: which directories may be written and which may not. +5. **Synchronize across platforms**: when the user configured multiple platforms, decide whether to change only the current platform or all platform agents. + +## Agent Pull Platforms + +If an agent file contains a prelude for "read task/context after startup," do not remove those steps when editing. Otherwise the agent will work only from chat context and bypass Trellis's core mechanism. + +## Hook Push Platforms + +If context is injected by a hook, the agent file should still retain responsibility boundaries. Do not remove PRD/spec requirements from the agent just because a hook injects context. diff --git a/.trae/skills/trellis-meta/references/customize-local/change-context-loading.md b/.trae/skills/trellis-meta/references/customize-local/change-context-loading.md new file mode 100644 index 0000000..002a259 --- /dev/null +++ b/.trae/skills/trellis-meta/references/customize-local/change-context-loading.md @@ -0,0 +1,84 @@ +# Change Local Context Loading + +Context loading determines when AI reads workflow, task, spec, research, workspace, and git status. Read this page when the user says "AI does not know the current task," "the agent did not read specs," or "there is too much/too little context." + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/scripts/get_context.py` +3. `.trellis/scripts/common/session_context.py` +4. `.trellis/scripts/common/task_context.py` +5. `.trellis/scripts/common/active_task.py` +6. Current platform hooks or agent files +7. The current task's `implement.jsonl` / `check.jsonl` + +## Context Sources + +| Source | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow and next-action hints. | +| `.trellis/tasks/<task>/prd.md` | Current task requirements. | +| `.trellis/tasks/<task>/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. | +| `.trellis/workspace/` | Session records. | +| git status | Current working tree changes. | + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Inject more/less information in new sessions | `session_context.py` or the platform `session-start` hook. | +| Change hints on each user input | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The `inject-workflow-state` hook is parser-only and reads the block verbatim. | +| Agent did not read specs | Task JSONL, agent prelude, `inject-subagent-context` hook. | +| Active task is lost | `active_task.py` and platform session identity propagation. | +| Change JSONL validation rules | `task_context.py`. | + +## JSONL Rules + +`implement.jsonl` / `check.jsonl` are the key context loading interface: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-x/research/api.md", "reason": "API research"} +``` + +Include only spec/research files. Do not put code files that will be modified into these manifests; agents read code files themselves during implementation. + +## Change Session Context + +If the user wants every new session to see more project state, edit: + +- `.trellis/scripts/common/session_context.py` +- the corresponding platform `session-start` hook + +Context cannot grow without bound. Prefer injecting indexes and paths so the AI can read detailed files on demand. + +## Change Sub-Agent Context + +First determine which mode the platform uses: + +- hook push: edit the `inject-subagent-context` hook. +- agent pull: edit the read steps in the corresponding `trellis-implement` / `trellis-check` agent file. + +In both modes, make sure the agent ultimately reads: + +1. active task +2. 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 + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py list-context <task> +python3 ./.trellis/scripts/task.py validate <task> +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Confirm the task and JSONL are correct before editing hooks/agents. diff --git a/.trae/skills/trellis-meta/references/customize-local/change-hooks.md b/.trae/skills/trellis-meta/references/customize-local/change-hooks.md new file mode 100644 index 0000000..79aa5c5 --- /dev/null +++ b/.trae/skills/trellis-meta/references/customize-local/change-hooks.md @@ -0,0 +1,57 @@ +# Change Local Hooks + +Hooks are the automation layer that connects a platform to Trellis. When the user wants to change "when context is injected," "how shell commands inherit a session," or "which files are read before an agent starts," hooks are usually the edit point. + +## Read These Files First + +1. Target platform settings/config, such as `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`, `.trae/hooks.json` +2. Target platform hooks directory +3. `.trellis/scripts/common/active_task.py` +4. `.trellis/scripts/common/session_context.py` +5. `.trellis/workflow.md` + +## Common Hook Types + +| Hook | Purpose | +| --- | --- | +| session-start | Injects a Trellis overview when a session starts, clears, or compacts. | +| workflow-state | Injects a state hint on each user input. | +| sub-agent context | Injects PRD/spec/research before an agent starts. | +| shell session bridge | Lets `task.py` commands in shell see the same session identity. | + +## Modification Steps + +1. Find the hook registration in settings/config. +2. Confirm the registered script path exists. +3. Read the hook script and identify inputs, outputs, and called `.trellis/scripts/`. +4. Modify hook behavior. +5. If the hook depends on workflow content, synchronize `.trellis/workflow.md`. + +## Example: Change New-Session Injection Content + +First find the session-start hook: + +```text +.claude/settings.json +.claude/hooks/session-start.py +``` + +If the hook ultimately calls `.trellis/scripts/get_context.py` or `session_context.py`, editing the local script is usually more robust than hard-coding content in the hook. + +## Example: Agent Did Not Read JSONL + +First confirm: + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py validate <task> +``` + +If the task and JSONL are correct, determine whether the platform uses hook push or agent pull. For hook push, edit `inject-subagent-context`; for agent pull, edit the agent file. + +## Notes + +- Settings handle registration, hook scripts handle behavior; inspect both together. +- Different platforms support different hook events. Do not directly copy another platform's settings. +- Hooks should read project-local `.trellis/`; they should not depend on Trellis upstream source paths. +- Hook failures should produce visible errors so AI does not silently lose context. diff --git a/.trae/skills/trellis-meta/references/customize-local/change-skills-or-commands.md b/.trae/skills/trellis-meta/references/customize-local/change-skills-or-commands.md new file mode 100644 index 0000000..0ed8792 --- /dev/null +++ b/.trae/skills/trellis-meta/references/customize-local/change-skills-or-commands.md @@ -0,0 +1,123 @@ +# Change Local Skills, Commands, Prompts, And Workflows + +When the user wants to change AI entry points, auto-trigger rules, or explicit command behavior, edit skills, commands, prompts, or workflows in local platform directories. + +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 + +| Goal | Recommendation | +| --- | --- | +| AI should automatically know a capability | Add or modify a skill. | +| User wants to trigger manually with a command | Add or modify a command/prompt/workflow. | +| Team project conventions | Prefer `.trellis/spec/` or a project-local skill — 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 + +A skill is usually: + +```text +<skill-name>/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should be short and responsible for triggering/routing. Put long content in `references/` so AI can read it on demand. + +The frontmatter description should specify when to use the skill. Example: + +```yaml +description: "Use when customizing this project's deployment workflow and release checklist." +``` + +Do not write vague descriptions such as "helpful project skill"; they can trigger incorrectly. + +### 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: + +- How the user triggers it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +If a command only repeats workflow rules, prefer making it reference/read `.trellis/workflow.md` instead of maintaining a second copy of the flow. + +## Common Paths + +| Platform | Entry directories | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| 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/` | +| Factory Droid | `.factory/skills/`, `.factory/commands/` | +| Pi Agent | `.pi/skills/` | +| Reasonix | `.reasonix/skills/` (no separate commands dir; slash commands built into the platform) | +| ZCode | `.agents/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 — 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 (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/.trae/skills/trellis-meta/references/customize-local/change-spec-structure.md b/.trae/skills/trellis-meta/references/customize-local/change-spec-structure.md new file mode 100644 index 0000000..ee9a176 --- /dev/null +++ b/.trae/skills/trellis-meta/references/customize-local/change-spec-structure.md @@ -0,0 +1,83 @@ +# Change Local Spec Structure + +When the user wants to change the engineering conventions AI follows, add new spec layers, or adjust monorepo package mapping, edit `.trellis/spec/` and `.trellis/config.yaml`. + +## Read These Files First + +1. `.trellis/config.yaml` +2. `.trellis/spec/` +3. `.trellis/workflow.md` planning artifact guidance and Phase 3.3 +4. Current task `implement.jsonl` / `check.jsonl` + +## Common Needs + +| Need | Edit location | +| --- | --- | +| Add backend/frontend/docs/test spec layer | `.trellis/spec/<layer>/` or `.trellis/spec/<package>/<layer>/` | +| Add shared thinking guides | `.trellis/spec/guides/` | +| Adjust monorepo packages | `packages` in `.trellis/config.yaml` | +| Change default package | `default_package` in `.trellis/config.yaml` | +| Control spec scanning scope | `spec_scope` in `.trellis/config.yaml` | +| Make a task read a new spec | Task `implement.jsonl` / `check.jsonl` | + +## Add A Spec Layer + +Single-repository example: + +```text +.trellis/spec/security/ +├── index.md +└── auth.md +``` + +Monorepo example: + +```text +.trellis/spec/webapp/security/ +├── index.md +└── auth.md +``` + +`index.md` should include: + +- What code this layer applies to. +- Pre-Development Checklist. +- Quality Check. +- Links to specific guideline files. + +## Update Context + +Adding a spec does not mean every task automatically reads it. The current task must reference it in JSONL: + +```bash +python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/webapp/security/index.md" "Security conventions" +python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/webapp/security/index.md" "Security review rules" +``` + +## Change Monorepo Packages + +Example `.trellis/config.yaml`: + +```yaml +packages: + webapp: + path: apps/web + api: + path: apps/api +default_package: webapp +``` + +After editing, run: + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Use this output to confirm AI can see the correct packages and spec layers. + +## Notes + +- Specs are user project conventions and can be changed according to project needs. +- Do not put temporary task information into specs; put temporary information in the task. +- Do not put long-term conventions only in agents or commands; preserve them in specs. +- After changing spec structure, check whether existing task JSONL files still point to files that exist. diff --git a/.trae/skills/trellis-meta/references/customize-local/change-task-lifecycle.md b/.trae/skills/trellis-meta/references/customize-local/change-task-lifecycle.md new file mode 100644 index 0000000..a7a340f --- /dev/null +++ b/.trae/skills/trellis-meta/references/customize-local/change-task-lifecycle.md @@ -0,0 +1,90 @@ +# Change Local Task Lifecycle + +Task lifecycle includes creation, start, context configuration, finish, archive, parent/child tasks, and lifecycle hooks. The default customization targets are `.trellis/tasks/`, `.trellis/config.yaml`, and `.trellis/scripts/`. + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/config.yaml` +3. `.trellis/scripts/task.py` +4. `.trellis/scripts/common/task_store.py` +5. `.trellis/scripts/common/task_utils.py` +6. The current task's `.trellis/tasks/<task>/task.json` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Automatically sync an external system after task creation | `hooks.after_create` in `.trellis/config.yaml`. | +| Automatically update status after task start | `hooks.after_start` in `.trellis/config.yaml`. | +| Run a script after task finish | `hooks.after_finish` in `.trellis/config.yaml`. | +| Clean external resources after archive | `hooks.after_archive` in `.trellis/config.yaml`. | +| Change default task fields | `.trellis/scripts/common/task_store.py`. | +| Change task parsing/search | `.trellis/scripts/common/task_utils.py`. | +| Change active task behavior | `.trellis/scripts/common/active_task.py`. | + +## lifecycle hooks + +`.trellis/config.yaml` supports: + +```yaml +hooks: + after_create: + - "python3 .trellis/scripts/hooks/my_sync.py create" + after_start: + - "python3 .trellis/scripts/hooks/my_sync.py start" + after_finish: + - "python3 .trellis/scripts/hooks/my_sync.py finish" + after_archive: + - "python3 .trellis/scripts/hooks/my_sync.py archive" +``` + +Hook commands receive the `TASK_JSON_PATH` environment variable, pointing to the current task's `task.json`. Hook failures should usually warn, but not block the main task operation. + +## Change Task Fields + +If the user wants to add project-local fields, prefer putting them under `meta` in `task.json` to avoid breaking existing scripts' assumptions about standard fields. + +Example: + +```json +"meta": { + "linearIssue": "ENG-123", + "risk": "high" +} +``` + +If standard fields really need to change, inspect every local script that reads `task.json`. + +## Change Active Task + +Active task is session-level state stored in `.trellis/.runtime/sessions/`. Do not fall back to a global `.current-task` model. If the user wants to change active task behavior, edit: + +- `.trellis/scripts/common/active_task.py` +- platform hooks or shell session bridges +- active task descriptions in `.trellis/workflow.md` + +### `task.py create` Sets the Active Pointer + +`cmd_create` in `.trellis/scripts/common/task_store.py` calls `set_active_task` best-effort right after writing the new task directory. The behavior: + +- When the calling shell carries session identity (`TRELLIS_CONTEXT_ID` env var, or any platform-specific session env that `resolve_context_key` recognizes — see `active_task.py:_ENV_SESSION_KEYS`), the per-session pointer at `.trellis/.runtime/sessions/<context_key>.json` is rewritten to point at the new task. The task's `status=planning` and `[workflow-state:planning]` fires on the very next `UserPromptSubmit`. +- When session identity is unavailable (raw CLI invocation outside an AI session, or a platform that doesn't propagate identity to shell), the task directory is still created and `status=planning` is still written, but the active pointer is left untouched. The user can attach the task later with `task.py start <dir>` once they're back in an AI session. + +This makes `[workflow-state:planning]` the live breadcrumb during the brainstorm and JSONL curation work that follows `task.py create`. The pre-R7 behavior left the breadcrumb stuck on `no_task` until `task.py start`, so the planning block was effectively dead text. + +If you fork `task.py` to add a new creation path (e.g. an external import that bypasses `cmd_create`), audit whether your path also calls `set_active_task`. Without that call, your created tasks will not surface as active. The full status writer table is in `.trellis/spec/cli/backend/workflow-state-contract.md`. + +## Modification Steps + +1. Confirm the current task with `python3 ./.trellis/scripts/task.py current --source`. +2. Read the current task's `task.json` and confirm status and fields. +3. For configuration needs, edit `.trellis/config.yaml` first. +4. For script behavior needs, then edit `.trellis/scripts/`. +5. If the AI flow changed, synchronize `.trellis/workflow.md`. + +## Do Not + +- Do not directly edit `.trellis/.runtime/sessions/` to "fix" business state. +- Do not hard-code project-private fields into scripts; prefer `meta`. +- Do not default to asking the user to fork Trellis CLI. diff --git a/.trae/skills/trellis-meta/references/customize-local/change-workflow.md b/.trae/skills/trellis-meta/references/customize-local/change-workflow.md new file mode 100644 index 0000000..337c985 --- /dev/null +++ b/.trae/skills/trellis-meta/references/customize-local/change-workflow.md @@ -0,0 +1,65 @@ +# Change Local Workflow + +When the user wants to change Trellis phases, next-action hints, whether to create tasks, whether to use sub-agents, or when to check/wrap up, edit `.trellis/workflow.md` first. + +## Read These Files First + +1. `.trellis/workflow.md` +2. Entry files for the current platform, such as skills/commands/prompts/workflows +3. The current task's `task.json` and `prd.md` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Change phase names or phase order | `Phase Index` and the corresponding Phase sections. | +| Change whether to create a task when there is no task | `[workflow-state:no_task]` state block. | +| Change the next step during planning | Phase 1 and `[workflow-state:planning]`. | +| Change whether an agent is required during in_progress | Phase 2 and `[workflow-state:in_progress]`. | +| Change wrap-up after completion | Phase 3 and `[workflow-state:completed]`. | +| Change which skill a user intent triggers | `Skill Routing` table. | + +## Modification Steps + +1. Find the relevant section in `.trellis/workflow.md`. +2. When changing rules, keep explicit trigger conditions and next actions. +3. If adding or renaming a skill/agent, synchronize the corresponding files in platform directories. +4. Workflow-state changes only need an edit to the `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook is parser-only — it reads whatever you put in the block. Keep the opening and closing tags' STATUS strings identical (`[workflow-state:foo]…[/workflow-state:foo]`); mismatched STATUS pairs are silently dropped. +5. Make the AI reread `.trellis/workflow.md`; do not keep using rules from the old conversation. + +## Example: Relax Task Creation Requirements + +To change when task creation can be skipped, usually edit `[workflow-state:no_task]`: + +```md +[workflow-state:no_task] +Task is not required when the answer is a one-reply explanation, no files are changed, and no research is needed. +[/workflow-state:no_task] +``` + +If the formal Phase 1 flow also needs to change, synchronize the Phase 1 section. + +## Example: One Platform Does Not Use Sub-Agents + +If the user wants only one platform to avoid sub-agents, first confirm whether that platform has a separate group in the workflow. Then change Phase 2 routing for that platform group instead of deleting all `trellis-implement` / `trellis-check` instructions across platforms. + +## `/trellis:continue` Route Table + +`/trellis:continue` resumes a task by deciding which phase step to load next. The decision combines `task.json.status` with the presence of artifacts inside the task directory. The mapping is fixed in the command itself; forks that add custom statuses must extend both the workflow.md tag block and this table. + +| `status` | Artifact state | Resume at | +| --- | --- | --- | +| `planning` | `prd.md` missing | Phase 1.1 (load `trellis-brainstorm`) | +| `planning` | 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.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. + +## Notes + +`.trellis/workflow.md` is the local project workflow, not an immutable template. The user can adapt it to team habits. After editing it, platform entry files may still contain old descriptions, so inspect them too. diff --git a/.trae/skills/trellis-meta/references/customize-local/overview.md b/.trae/skills/trellis-meta/references/customize-local/overview.md new file mode 100644 index 0000000..b75d208 --- /dev/null +++ b/.trae/skills/trellis-meta/references/customize-local/overview.md @@ -0,0 +1,55 @@ +# Local Customization Overview + +This directory is for local AI working in a user project where Trellis was installed through npm and `trellis init` has already been run. The AI should modify generated `.trellis/` and platform directories inside the project, not Trellis CLI upstream source code. + +## First Determine What The User Actually Wants To Change + +| User wording | Read first | +| --- | --- | +| "Change the Trellis flow / phases / next prompt" | `change-workflow.md` | +| "Change task creation, status, archive, or hooks" | `change-task-lifecycle.md` | +| "AI did not read context / change injected content" | `change-context-loading.md` | +| "A platform hook is not behaving as expected" | `change-hooks.md` | +| "Change implement/check/research agent behavior" | `change-agents.md` | +| "Add a skill/command/workflow/prompt" | `change-skills-or-commands.md` | +| "Adjust the project spec structure" | `change-spec-structure.md` | +| "Add team conventions and local notes" | `add-project-local-conventions.md` | + +## General Operation Order + +1. **Confirm platform and directories**: inspect which directories exist, such as `.claude/`, `.codex/`, `.cursor/`, `.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. +5. **Synchronize semantics**: if a shared flow changes, check whether platform entry points also need changes; if a platform entry changes, check whether `.trellis/workflow.md` still agrees. + +## Local File Priority + +| Layer | Files | +| --- | --- | +| Workflow | `.trellis/workflow.md` | +| Project configuration | `.trellis/config.yaml` | +| Task material | `.trellis/tasks/<task>/` | +| Project specs | `.trellis/spec/` | +| Runtime scripts | `.trellis/scripts/` | +| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.zcode/`, and similar directories | +| Shared skill | `.agents/skills/` | + +## Things Not To Do By Default + +- Do not edit the global npm install directory. +- Do not edit `node_modules/@mindfoldhq/trellis`. +- Do not assume the user has the Trellis GitHub repository. +- Do not overwrite local files already modified by the user with default templates. +- Do not put team project rules into public `trellis-meta`; project rules belong in `.trellis/spec/` or a local skill. + +## When To Inspect Upstream Source + +Switch to an upstream source-code perspective only when the user explicitly expresses one of these goals: + +- "I want to open a PR to Trellis" +- "I want to change npm package publish contents" +- "I want to fork Trellis" +- "I want to modify the generation logic for `trellis init/update`" + +Otherwise, default to modifying local Trellis files inside the user project. diff --git a/.trae/skills/trellis-meta/references/local-architecture/bundled-skills.md b/.trae/skills/trellis-meta/references/local-architecture/bundled-skills.md new file mode 100644 index 0000000..1c819cb --- /dev/null +++ b/.trae/skills/trellis-meta/references/local-architecture/bundled-skills.md @@ -0,0 +1,146 @@ +# 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` | +| 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`, 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/.trae/skills/trellis-meta/references/local-architecture/context-injection.md b/.trae/skills/trellis-meta/references/local-architecture/context-injection.md new file mode 100644 index 0000000..4a7517b --- /dev/null +++ b/.trae/skills/trellis-meta/references/local-architecture/context-injection.md @@ -0,0 +1,68 @@ +# Local Context Injection System + +Trellis context injection aims to make AI read the right files at the right time instead of relying on model memory. In a user project, injection is implemented by `.trellis/` scripts together with platform hooks, agents, and skills. + +## Injected Context Types + +| Type | Source | Purpose | +| --- | --- | --- | +| session context | `.trellis/scripts/get_context.py` | Current developer, git status, active task, active tasks, journal, packages. | +| workflow context | `.trellis/workflow.md` | Current Trellis flow and next action. | +| spec context | `.trellis/spec/` + task JSONL | Specs that must be followed during implementation/checking. | +| task context | `.trellis/tasks/<task>/prd.md`, `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 + +Platforms with session-start support inject a Trellis overview when a session starts, clears, compacts, or receives a similar event. Injected content usually includes: + +- workflow summary. +- current task status. +- active tasks. +- spec index paths. +- developer identity and git status. + +If the user feels the AI does not know the current task in a new session, first check whether the platform's session-start hook or equivalent mechanism is installed and running. + +## workflow-state + +workflow-state is a lightweight hint injected around each user turn. Based on current task status, it selects a block from `.trellis/workflow.md`, such as `no_task`, `planning`, `in_progress`, or `completed`. + +If the user wants to change "what the AI should do next in a given state," edit the corresponding state block in `.trellis/workflow.md` first. + +## sub-agent context + +Implement and check agents need task context. Trellis has two loading modes: + +1. **hook push**: a platform hook injects 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 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 + +`implement.jsonl` and `check.jsonl` contain one JSON object per line: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend rules"} +``` + +Readers should skip seed rows without a `file` field. When configuring JSONL, the AI should include only spec/research files, not pre-register code files that will be modified. + +## Active Task And Context Key + +Active task state lives in `.trellis/.runtime/sessions/` and is isolated per session. Hooks try to resolve the context key from platform events, environment variables, transcript paths, or `TRELLIS_CONTEXT_ID`. + +If shell commands cannot see the same context key, `task.py current --source` may report no active task. In that case, check whether the platform passes session identity into the shell instead of hand-writing a global current-task file. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change session-start injected content | The platform's `session-start` hook or plugin file. | +| Change per-turn workflow-state rules | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The platform workflow-state hook parses these blocks verbatim and embeds no fallback text. | +| Change how sub-agents read context | Platform agent definitions, the `inject-subagent-context` hook, or agent preludes. | +| Change JSONL validation/display | `.trellis/scripts/common/task_context.py`. | +| Change active task resolution | `.trellis/scripts/common/active_task.py`. | + +When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct task artifacts/spec/research. diff --git a/.trae/skills/trellis-meta/references/local-architecture/generated-files.md b/.trae/skills/trellis-meta/references/local-architecture/generated-files.md new file mode 100644 index 0000000..5e53865 --- /dev/null +++ b/.trae/skills/trellis-meta/references/local-architecture/generated-files.md @@ -0,0 +1,80 @@ +# Local Files Generated After Init + +`trellis init` writes the Trellis runtime into the user project. Later, `trellis update` tries to update Trellis-managed template files, but it uses `.trellis/.template-hashes.json` to determine which files have already been modified by the user. + +This page only describes files that are visible and editable inside the user project. + +## `.trellis/` + +```text +.trellis/ +├── workflow.md +├── config.yaml +├── .developer +├── .version +├── .template-hashes.json +├── .runtime/ +├── scripts/ +├── spec/ +├── tasks/ +└── workspace/ +``` + +| Path | Usually editable? | Notes | +| --- | --- | --- | +| `.trellis/workflow.md` | Yes | Local workflow documentation and AI routing rules. | +| `.trellis/config.yaml` | Yes | Project configuration, hooks, packages, journal line limits, and related settings. | +| `.trellis/spec/` | Yes | Project specs, intended to be updated regularly by users and AI. | +| `.trellis/tasks/` | Yes | Task material and research artifacts, maintained by the task workflow. | +| `.trellis/workspace/` | Yes | Session records, usually written by `add_session.py`. | +| `.trellis/scripts/` | Carefully | Local runtime. It can be customized, but only after understanding the call chain. | +| `.trellis/.runtime/` | No | Runtime state, usually written automatically by hooks/scripts. | +| `.trellis/.developer` | Carefully | Current developer identity. | +| `.trellis/.version` | No | Trellis version record used by update/migration logic. | +| `.trellis/.template-hashes.json` | No | Template hash record. Do not hand-write business rules here. | + +## Platform Directories + +Different platforms generate different directories. Common categories: + +| Category | Example paths | Purpose | +| --- | --- | --- | +| hooks | `.claude/hooks/`, `.codex/hooks/`, `.cursor/hooks/` | Inject session context, workflow-state, and sub-agent context. | +| settings | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.trae/hooks.json` | Tell the platform when to run hooks or plugins. | +| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/`, `.zcode/cli/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/`, `.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. + +## Meaning Of Template Hashes + +`.trellis/.template-hashes.json` records the content hash from the last time Trellis wrote a template file. `trellis update` uses it to distinguish three cases: + +| Case | Update behavior | +| --- | --- | +| File was not modified by the user | It can be updated automatically. | +| File was modified by the user | Prompt the user to overwrite, keep, or generate `.new`. | +| File is no longer a current template | It may be deleted, renamed, or preserved according to migration rules. | + +When an AI customizes local Trellis files, it does not need to maintain hashes manually. It is normal for Trellis update to recognize the result as "modified by the user." + +## Local Customization Boundaries + +Editable by default: + +- `.trellis/workflow.md` +- `.trellis/config.yaml` +- `.trellis/spec/**` +- `.trellis/scripts/**` +- Platform hooks, settings, agents, skills, commands, prompts, and workflows + +Do not edit by default: + +- Global npm install directory +- `node_modules/@mindfoldhq/trellis` +- Trellis GitHub repository source code +- Concrete state files under `.trellis/.runtime/**` +- Hash contents inside `.trellis/.template-hashes.json` + +Switch to the Trellis CLI source-code perspective only when the user explicitly wants to contribute upstream. diff --git a/.trae/skills/trellis-meta/references/local-architecture/multi-agent-channel.md b/.trae/skills/trellis-meta/references/local-architecture/multi-agent-channel.md new file mode 100644 index 0000000..6df61eb --- /dev/null +++ b/.trae/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/.trae/skills/trellis-meta/references/local-architecture/overview.md b/.trae/skills/trellis-meta/references/local-architecture/overview.md new file mode 100644 index 0000000..e97cab8 --- /dev/null +++ b/.trae/skills/trellis-meta/references/local-architecture/overview.md @@ -0,0 +1,51 @@ +# Local Trellis Architecture Overview + +`trellis-meta` is for user projects that have already run `trellis init`. The user's machine usually has only the npm-installed `trellis` command plus the Trellis files generated inside the project; it may not have the Trellis CLI source code. + +Therefore, when an AI uses this skill, the default customization target is local files inside the user project: + +- `.trellis/`: workflow, tasks, specs, memory, scripts, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.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. + +## Local System Model + +Trellis provides three layers inside a user project: + +1. **Workflow layer**: `.trellis/workflow.md` defines phases, routing, next actions, and prompt blocks. +2. **Persistence layer**: `.trellis/tasks/`, `.trellis/spec/`, and `.trellis/workspace/` store tasks, specs, and session memory. +3. **Platform integration layer**: hooks, settings, agents, skills, commands, prompts, and workflows in platform directories connect the Trellis workflow to different AI tools. + +All three layers live inside the user project, so an AI can read and modify them directly. + +## Core Paths + +| Path | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow phases, skill routing, and workflow-state prompt blocks. | +| `.trellis/config.yaml` | Project configuration, task lifecycle hooks, monorepo package configuration, and journal configuration. | +| `.trellis/spec/` | The user's project-specific coding conventions and thinking guides. | +| `.trellis/tasks/` | Each task's PRD, technical notes, research files, and JSONL context. | +| `.trellis/workspace/` | Per-developer journals and cross-session memory. | +| `.trellis/scripts/` | Local Python runtime used by commands, hooks, and context injection. | +| `.trellis/.runtime/` | Session-level runtime state, such as the current task pointer. | +| `.trellis/.template-hashes.json` | Template hashes for Trellis-managed files, used by update to determine whether local files were modified by the user. | + +## AI Customization Principles + +1. **Find the local source of truth first**: Do not edit from memory. Read `.trellis/workflow.md`, `.trellis/config.yaml`, the relevant platform directory, and related task files first. +2. **Edit the user project, not the npm package cache**: Modify generated files inside the project, not `node_modules` or the global npm install directory. +3. **Keep platform files aligned with `.trellis/`**: If workflow routing changes, also check whether platform skills or commands still describe the same flow. +4. **Put project-specific rules in `.trellis/spec/` or a local skill**: Do not put team conventions into `trellis-meta`. +5. **Preserve user changes**: If a file was already modified locally, work from the current content instead of overwriting it with a default template. + +## How To Use This Directory + +- To understand which files exist after init, read `generated-files.md`. +- To change phases, routing, or next actions, read `workflow.md`. +- To change the task model, JSONL context, or active task behavior, read `task-system.md`. +- To change coding convention injection, read `spec-system.md`. +- To understand journals and cross-session memory, read `workspace-memory.md`. +- To change hooks or sub-agent context loading, read `context-injection.md`. diff --git a/.trae/skills/trellis-meta/references/local-architecture/spec-system.md b/.trae/skills/trellis-meta/references/local-architecture/spec-system.md new file mode 100644 index 0000000..38fdf14 --- /dev/null +++ b/.trae/skills/trellis-meta/references/local-architecture/spec-system.md @@ -0,0 +1,102 @@ +# Local Spec System + +`.trellis/spec/` is the user's project-specific engineering spec library. Trellis is not about making AI memorize conventions; it injects relevant specs or requires the AI to read them at the right time. + +## Directory Model + +A common single-repository structure: + +```text +.trellis/spec/ +├── backend/ +│ ├── index.md +│ └── ... +├── frontend/ +│ ├── index.md +│ └── ... +└── guides/ + ├── index.md + └── ... +``` + +A common monorepo structure: + +```text +.trellis/spec/ +├── cli/ +│ ├── backend/ +│ │ ├── index.md +│ │ └── ... +│ └── unit-test/ +│ ├── index.md +│ └── ... +├── docs-site/ +│ └── docs/ +│ ├── index.md +│ └── ... +└── guides/ + ├── index.md + └── ... +``` + +`index.md` is the entry point for each layer. It should list the Pre-Development Checklist and Quality Check. Specific guidelines live in other Markdown files in the same directory. + +## Package Configuration + +`.trellis/config.yaml` can declare packages: + +```yaml +packages: + cli: + path: packages/cli + docs-site: + path: docs-site + type: submodule +default_package: cli +``` + +The AI can run: + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +This command lists packages and spec layers for the current project. Use this output as the reference when configuring context JSONL. + +## How Specs Enter Tasks + +Before a task enters implementation, 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"} +{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Test expectations"} +``` + +Sub-agents or platform preludes read these JSONL files and load the referenced specs. On platforms without sub-agent support, the AI should read the relevant specs directly according to the workflow. + +## What Specs Should Contain + +Specs should contain executable engineering conventions for the project, not generic best practices: + +- Where files should live. +- How error handling should be expressed. +- Input/output contracts for APIs, hooks, and commands. +- Patterns that are forbidden. +- Cases that require tests. +- Project-specific pitfalls and how to avoid them. + +When the AI learns a new rule during implementation or debugging, it should update `.trellis/spec/` rather than only summarizing it in chat. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Add a new spec layer | `.trellis/spec/<package>/<layer>/index.md` and corresponding guideline files. | +| Change monorepo spec mapping | `packages` / `default_package` / `spec_scope` in `.trellis/config.yaml`. | +| Change which specs AI reads before implementation | The task's `implement.jsonl`. | +| Change which specs AI reads during checking | The task's `check.jsonl`. | +| Change when specs should be updated | Phase 3.3 in `.trellis/workflow.md` and the `trellis-update-spec` skill. | + +## Boundaries + +`.trellis/spec/` is the user's project specification, not a permanent copy of Trellis built-in templates. The AI should encourage the user to update it according to the actual project code instead of treating Trellis default templates as immutable documents. diff --git a/.trae/skills/trellis-meta/references/local-architecture/task-system.md b/.trae/skills/trellis-meta/references/local-architecture/task-system.md new file mode 100644 index 0000000..7133495 --- /dev/null +++ b/.trae/skills/trellis-meta/references/local-architecture/task-system.md @@ -0,0 +1,130 @@ +# Local Task System + +The Trellis task system is stored entirely under `.trellis/tasks/` in the user project. Each task is a directory containing requirements, context, research, state, and relationship information. + +## Task Directory Structure + +```text +.trellis/tasks/ +├── 04-28-example-task/ +│ ├── task.json +│ ├── prd.md +│ ├── design.md +│ ├── implement.md +│ ├── implement.jsonl +│ ├── check.jsonl +│ └── research/ +└── archive/ + └── 2026-04/ +``` + +| File | Purpose | +| --- | --- | +| `task.json` | Task metadata: status, assignee, priority, branch, parent/child tasks, and similar fields. | +| `prd.md` | Requirements, 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. | + +## `task.json` + +`task.json` records task status and metadata. Common fields: + +| Field | Meaning | +| --- | --- | +| `id` / `name` / `title` | Task identity and title. | +| `status` | Status such as `planning`, `in_progress`, `review`, or `completed`. | +| `priority` | `P0`, `P1`, `P2`, `P3`. | +| `creator` / `assignee` | Creator and assignee. | +| `package` | Target package in a monorepo; may be empty. | +| `branch` / `base_branch` | Working branch and PR target branch. | +| `children` / `parent` | Parent/child task relationships. | +| `commit` / `pr_url` | Commit and PR information after completion. | +| `meta` | Extension fields. | + +## 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 + +The user sees a "current task," but Trellis stores active task state per session. + +```text +.trellis/.runtime/sessions/<context-key>.json +``` + +`task.py start` writes the task path into the runtime session file for the current session. `task.py current --source` shows the current task and where it came from. Different AI windows can point to different tasks without overwriting each other. + +If the platform or shell environment has no stable session identity, `task.py start` may be unable to set the active task. The AI should read the error, inspect the platform hook/session environment, and not fall back to a shared global pointer. + +## JSONL Context + +`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first. They do not replace `implement.md`; `implement.md` is the human-readable execution plan. + +Format: + +```jsonl +{"file": ".trellis/spec/cli/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-example/research/api.md", "reason": "API research"} +``` + +Rules: + +- Include spec and research files. +- Do not include code files that are about to be modified. +- Do not treat temporary conclusions in chat as the only context. +- Seed rows have no `file` field; they only prompt the AI to fill in real entries. + +## Common Commands + +```bash +python3 ./.trellis/scripts/task.py create "<title>" --slug <slug> +python3 ./.trellis/scripts/task.py start <task> +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py add-context <task> implement <file> <reason> +python3 ./.trellis/scripts/task.py validate <task> +python3 ./.trellis/scripts/task.py finish +python3 ./.trellis/scripts/task.py archive <task> +``` + +When modifying the task system, the AI should prefer script commands to maintain structure. Edit JSON/Markdown directly only when scripts do not cover the need. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change the default task template | `.trellis/scripts/common/task_store.py` and task creation instructions. | +| Change status semantics | `.trellis/workflow.md`, workflow-state hook logic, and task usage conventions. | +| Add task lifecycle actions | `hooks.after_*` in `.trellis/config.yaml`. | +| Change context rules | 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/.trae/skills/trellis-meta/references/local-architecture/workflow.md b/.trae/skills/trellis-meta/references/local-architecture/workflow.md new file mode 100644 index 0000000..f0659ff --- /dev/null +++ b/.trae/skills/trellis-meta/references/local-architecture/workflow.md @@ -0,0 +1,75 @@ +# Local Workflow System + +`.trellis/workflow.md` is the Trellis workflow source of truth inside the user project. An AI does not need Trellis source code to understand how the current project should move tasks forward; this file is enough. + +## File Responsibilities + +`.trellis/workflow.md` has three responsibilities: + +1. **Explain workflow phases**: Plan, Execute, Finish. +2. **Define skill routing**: which skill or agent the AI should use when the user expresses a certain intent. +3. **Provide workflow-state prompt blocks**: hooks can inject the prompt block for the current state into the conversation. + +## Current Phase Model + +```text +Phase 1: Plan -> clarify what to build, produce prd.md and required research +Phase 2: Execute -> implement against the PRD and specs, then check +Phase 3: Finish -> final verification, preserve lessons, and wrap up +``` + +Each phase contains numbered steps, such as `1.3 Configure context`. These numbers are not runtime fields in `task.json`; they are workflow structure for AI and humans to read. + +## Skill Routing + +`workflow.md` separates routing by platform capability: + +- Platforms with sub-agent support: dispatch `trellis-implement` by default for implementation and `trellis-check` for checking. +- Platforms without sub-agent support: the main session reads skills such as `trellis-before-dev`, then executes directly. + +When changing local AI behavior, update the routing descriptions in `workflow.md` first, then check whether the corresponding platform skill, command, or agent files need to stay in sync. + +## Workflow-State Prompt Blocks + +The bottom of `workflow.md` can contain state blocks like this: + +```text +[workflow-state:no_task] +... +[/workflow-state:no_task] +``` + +Hooks choose the right block based on current task status and inject it into the conversation. Common states include: + +| State | Meaning | +| --- | --- | +| `no_task` | The current session has no active task. | +| `planning` | The task is still in requirements, research, or context configuration. | +| `in_progress` | The task has entered implementation and checking. | +| `completed` | The task is complete and waiting for wrap-up or archive. | + +If the user wants to change policies such as "whether to create a task when there is no task," "when task creation may be skipped," or "whether sub-agents are required," edit these state blocks and the routing table above them. + +## Local Modification Patterns + +Common changes: + +| Goal | Edit point | +| --- | --- | +| Add a phase | Update the Phase Index, phase body, routing, and state blocks. | +| Change task creation policy | Update the `no_task` state block and Phase 1 description. | +| Change the default implementation/check path | Update Phase 2 and skill routing. | +| Change the wrap-up flow | Update Phase 3 and `finish-work` related descriptions. Note the current split: Phase 3.4 = AI-driven code commits (batched, user-confirmed), Phase 3.5 = `/finish-work` (archive + record session). `/finish-work` refuses to run if the working tree is dirty. | +| Change platform differences | Update routing descriptions grouped by platform. | + +After editing, make the AI reread `.trellis/workflow.md`; do not assume the flow from the old conversation is still valid. + +## Relationship To Platform Files + +`workflow.md` is the semantic center of the local workflow, but each platform can also have its own entry files: + +- skills, such as `trellis-brainstorm` and `trellis-check`. +- commands/prompts/workflows, such as continue and finish-work. +- hooks, such as session-start or workflow-state injection. + +If only `workflow.md` changes, platform entry files may still contain old language. When the user wants to change "what the AI actually does," also inspect the relevant platform directory. diff --git a/.trae/skills/trellis-meta/references/local-architecture/workspace-memory.md b/.trae/skills/trellis-meta/references/local-architecture/workspace-memory.md new file mode 100644 index 0000000..c2958f2 --- /dev/null +++ b/.trae/skills/trellis-meta/references/local-architecture/workspace-memory.md @@ -0,0 +1,71 @@ +# Local Workspace Memory System + +`.trellis/workspace/` stores cross-session memory. Its purpose is to let AI and humans understand what happened before across different windows and different days. + +## Directory Structure + +```text +.trellis/workspace/ +├── index.md +└── <developer>/ + ├── index.md + ├── journal-1.md + └── journal-2.md +``` + +| File | Purpose | +| --- | --- | +| `.trellis/.developer` | Current developer identity. | +| `.trellis/workspace/index.md` | Global workspace overview. | +| `.trellis/workspace/<developer>/index.md` | Session index for a developer. | +| `.trellis/workspace/<developer>/journal-N.md` | Session journal. | + +## Developer Identity + +Run this the first time: + +```bash +python3 ./.trellis/scripts/init_developer.py <name> +``` + +This creates `.trellis/.developer` and the corresponding workspace directory. The AI should not change developer identity casually; if the identity is wrong, first confirm who is using the current project. + +## Journal + +`journal-N.md` records completed or partially completed work from each session. By default, each journal holds about 2000 lines; after that it rotates to the next file. + +Common command for recording a session: + +```bash +python3 ./.trellis/scripts/add_session.py \ + --title "Session title" \ + --summary "What changed" \ + --commit "abc1234" +``` + +Planning or review work without a commit can also be recorded by using `--no-commit` or an empty commit value. + +## Relationship Between Workspace Memory And Tasks + +| System | What it stores | +| --- | --- | +| `.trellis/tasks/` | Requirements, design, research, and state for a specific task. | +| `.trellis/workspace/` | Work records across tasks and sessions. | +| `.trellis/spec/` | Engineering knowledge preserved as long-term conventions. | + +If information is only useful for the current task, put it in the task directory. +If information describes what happened in the current session, put it in the workspace journal. +If information should be followed every time code is written in the future, put it in spec. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change maximum journal lines | `max_journal_lines` in `.trellis/config.yaml`. | +| Change session auto-commit message | `session_commit_message` in `.trellis/config.yaml`. | +| Change session content format | `.trellis/scripts/add_session.py`. | +| Change how workspace is displayed in context | `.trellis/scripts/common/session_context.py`. | + +## AI Usage Rules + +The AI should not treat workspace as the only source of truth. When resuming a task, read the current task first, then use workspace for background. After a task is complete, record important process notes in workspace; if long-term rules emerged, update spec. diff --git a/.trae/skills/trellis-meta/references/platform-files/agents.md b/.trae/skills/trellis-meta/references/platform-files/agents.md new file mode 100644 index 0000000..a07cae7 --- /dev/null +++ b/.trae/skills/trellis-meta/references/platform-files/agents.md @@ -0,0 +1,82 @@ +# Agents + +Trellis agent files define specialized roles. Common Trellis agents in a user project are: + +- `trellis-research` +- `trellis-implement` +- `trellis-check` + +File locations and formats differ by platform, but responsibility boundaries should stay consistent. + +## Agent Responsibilities + +| Agent | Responsibility | +| --- | --- | +| `trellis-research` | Investigate the question and write findings into the current task's `research/`. | +| `trellis-implement` | Implement against `prd.md`, 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. + +## Common Paths + +| Platform | Agent path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | +| Reasonix | `.reasonix/skills/trellis-*/SKILL.md` (subagent frontmatter) | +| ZCode | `.zcode/cli/agents/trellis-*.md` | + +GitHub Copilot agent/prompt support is provided by a combination of directories such as `.github/agents/`, `.github/prompts/`, and `.github/skills/`; inspect the files actually generated in the user project. + +Main-session workflow platforms such as Kilo, Antigravity, and Devin may not have Trellis sub-agent files. They usually rely on workflows/skills to guide the main session. + +## Two Context Loading Modes + +### hook push + +The platform hook injects task context before the agent starts. The agent file itself can focus more on responsibilities and boundaries. + +Common on platforms that support agent hooks. + +### agent pull + +The agent file instructs the agent to read after startup: + +- `python3 ./.trellis/scripts/task.py current --source` +- `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. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Implement agent must follow extra restrictions | The platform's `trellis-implement` agent file. | +| Check agent must run project-specific commands | `trellis-check` agent file, and `.trellis/spec/` if needed. | +| Research agent must output a fixed format | `trellis-research` agent file. | +| Agent cannot read task context | Agent prelude or `inject-subagent-context` hook. | +| Add a project-specific agent | Platform agent directory + related workflow/command/skill entry point. | + +## Modification Principles + +1. **Keep responsibilities single-purpose**. Do not mix research, implement, and check responsibilities into one agent. +2. **Specify the read order**. Agents must know to start from the active task, 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. + +## Do Not Default To Editing Upstream Templates + +Local AI should default to modifying platform agent files inside the user project. Discuss upstream template source only when the user explicitly wants to contribute the change back to Trellis. diff --git a/.trae/skills/trellis-meta/references/platform-files/hooks-and-settings.md b/.trae/skills/trellis-meta/references/platform-files/hooks-and-settings.md new file mode 100644 index 0000000..d4ae8f6 --- /dev/null +++ b/.trae/skills/trellis-meta/references/platform-files/hooks-and-settings.md @@ -0,0 +1,72 @@ +# Hooks And Settings + +Hooks/settings are the entry layer that connects a platform to Trellis. They decide which scripts, plugins, or extensions a platform runs for which events. + +## Settings Responsibilities + +settings/config files usually register: + +- session-start hook: injects a Trellis overview when a new session starts or context resets. +- workflow-state hook: parses `[workflow-state:STATUS]` blocks from `.trellis/workflow.md` and emits the body matching the current task `status` on each user input. Parser-only; the script does not embed fallback content. +- sub-agent context hook: injects task context when implementation/check/research agents start. +- shell/session bridge: lets shell commands see the same Trellis session identity. +- platform plugin or extension entry points. + +Common files: + +| Platform | settings/config | +| --- | --- | +| Claude Code | `.claude/settings.json` | +| Cursor | `.cursor/hooks.json` | +| Codex | `.codex/hooks.json`, `.codex/config.toml` | +| OpenCode | `.opencode/package.json`, `.opencode/plugins/*` | +| Kiro | `.kiro/hooks/` + platform config | +| Gemini CLI | `.gemini/settings.json` | +| Qoder | `.qoder/settings.json` | +| CodeBuddy | `.codebuddy/settings.json` | +| GitHub Copilot | `.github/copilot/hooks.json` | +| Factory Droid | `.factory/settings.json` | +| Pi Agent | `.pi/settings.json`, `.pi/extensions/trellis/` | +| Trae IDE | `.trae/hooks.json` | + +Reasonix and ZCode are pull-based platforms that do not use hooks or settings files; their agent files contain prelude instructions to read context after startup. + +Whether these files exist in a project depends on which `trellis init --<platform>` flags the user ran. + +## Hook Script Types + +| Script | Purpose | +| --- | --- | +| `session-start.py` | Generates session-start context. | +| `inject-workflow-state.py` | Parses `[workflow-state:STATUS]` blocks in `.trellis/workflow.md` and emits the body matching the current task status. Falls back to `Refer to workflow.md for current step.` when no matching block exists. | +| `inject-subagent-context.py` | Injects PRD, JSONL context, and related spec/research into sub-agents. | +| `inject-shell-session-context.py` | Lets shell commands inherit Trellis session identity. | + +Not every platform has every hook. Do not copy files from another platform just because a platform lacks a hook; first confirm whether that platform supports the corresponding event. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| AI should see more/less context in a new session | Platform `session-start` hook. | +| Per-turn hint policy should change | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook parses workflow.md verbatim — no script edit required. | +| Sub-agent cannot read PRD/spec | `inject-subagent-context` hook or agent prelude. | +| `task.py current` in shell has no active task | Shell/session bridge hook or platform environment variable configuration. | +| Disable an automatic injection | The corresponding hook registration in settings/config. | + +## Modification Principles + +1. **Settings wire things up; hooks define behavior**. If only the hook changes, the platform may never call it. If only settings change, behavior may not change. +2. **Confirm platform event names first**. Different platforms use different names for SessionStart, UserPromptSubmit, AgentSpawn, shell execution, and similar events. +3. **Hooks read local `.trellis/`, not upstream source**. `.trellis/scripts/` and `.trellis/workflow.md` in the user project are the default targets. +4. **Errors must be visible**. Hook failures should tell the user what was not injected instead of silently leaving the AI without context. + +## Troubleshooting Path + +If the user says "AI did not read Trellis state": + +1. Check whether the platform settings register the hook. +2. Check whether the hook file exists. +3. Manually run the `.trellis/scripts/get_context.py` or `task.py current --source` command that the hook depends on. +4. Check whether active task state exists in `.trellis/.runtime/sessions/`. +5. Check whether the platform shell passes session identity. diff --git a/.trae/skills/trellis-meta/references/platform-files/overview.md b/.trae/skills/trellis-meta/references/platform-files/overview.md new file mode 100644 index 0000000..e403858 --- /dev/null +++ b/.trae/skills/trellis-meta/references/platform-files/overview.md @@ -0,0 +1,59 @@ +# Platform Files Overview + +Trellis connects the same local architecture to different AI tools. `.trellis/` stores the shared runtime; platform directories store adapter files that define how each AI tool enters Trellis. + +When a local AI modifies Trellis, it should distinguish two file categories first: + +- **Shared files**: `.trellis/workflow.md`, `.trellis/tasks/`, `.trellis/spec/`, `.trellis/scripts/`. +- **Platform files**: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.trae/`, `.kilocode/`, `.agent/`, `.devin/`, `.reasonix/`, `.zcode/`, and similar directories. + +Platform files do not store business state. They let the corresponding AI tool read Trellis state, call Trellis scripts, and load Trellis skills/agents/hooks. + +## Platform File Categories + +| Category | Common paths | Purpose | +| --- | --- | --- | +| settings/config | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json`, `.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/`, `.devin/workflows/` | Entry points explicitly invoked by the user. | + +## Three Platform Integration Modes + +### 1. Hook / Extension Driven + +These platforms can trigger scripts or plugins on specific events and actively inject Trellis context into AI. + +Common capabilities: + +- session-start injection of a `.trellis/` overview. +- workflow-state hints for each user turn. +- PRD/spec/research injection when sub-agents start. +- Shell commands inheriting session identity. + +To change "when the AI knows what," inspect hooks/plugins/extensions and settings first. + +### 2. Agent Prelude / Pull-Based + +Some platforms cannot reliably let hooks rewrite sub-agent prompts, so the agent file itself instructs the agent to read the active task, PRD, and JSONL context after startup. + +To change how sub-agents load context, inspect the agent files themselves. + +### 3. Main-Session Workflow + +Some platforms do not have Trellis sub-agent or hook capabilities. They rely on workflows/skills/commands to guide the main-session AI to read files, run scripts, and move tasks forward. + +To change behavior, inspect platform workflows/skills/commands and `.trellis/workflow.md`. + +## Local Modification Order + +When the user asks to customize behavior for a platform, the AI should inspect files in this order: + +1. Read `.trellis/workflow.md` to confirm the shared flow. +2. Read the target platform's settings/config to see which hooks/agents/skills/commands are registered. +3. Read the target platform's agents/skills/commands/hooks. +4. Modify the local file closest to the user's need. +5. If the change affects the shared flow, synchronize `.trellis/workflow.md` or `.trellis/spec/`. + +Do not modify only platform files and forget the shared workflow. Do not modify only `.trellis/workflow.md` and forget that platform entry points may still contain old descriptions. diff --git a/.trae/skills/trellis-meta/references/platform-files/platform-map.md b/.trae/skills/trellis-meta/references/platform-files/platform-map.md new file mode 100644 index 0000000..dddb743 --- /dev/null +++ b/.trae/skills/trellis-meta/references/platform-files/platform-map.md @@ -0,0 +1,88 @@ +# Platform File Map + +This page lists common Trellis file locations in a user project by platform. Whether a platform directory exists in an actual project depends on which `trellis init --<platform>` commands the user ran. + +## Matrix + +| Platform | CLI flag | Main directory | Skill directory | Agent directory | Hooks/extensions | +| --- | --- | --- | --- | --- | --- | +| Claude Code | `--claude` | `.claude/` | `.claude/skills/` | `.claude/agents/` | `.claude/hooks/` + `.claude/settings.json` | +| Cursor | `--cursor` | `.cursor/` | `.cursor/skills/` | `.cursor/agents/` | `.cursor/hooks.json` + `.cursor/hooks/` | +| OpenCode | `--opencode` | `.opencode/` | `.opencode/skills/` | `.opencode/agents/` | `.opencode/plugins/` | +| Codex | `--codex` | `.codex/` | `.agents/skills/` | `.codex/agents/` | `.codex/hooks/` + `.codex/hooks.json` | +| Kilo | `--kilo` | `.kilocode/` | `.kilocode/skills/` | Usually none | `.kilocode/workflows/` | +| Kiro | `--kiro` | `.kiro/` | `.kiro/skills/` | `.kiro/agents/` | `.kiro/hooks/` | +| Gemini CLI | `--gemini` | `.gemini/` | `.agents/skills/` | `.gemini/agents/` | `.gemini/settings.json` + `.gemini/hooks/` | +| Antigravity | `--antigravity` | `.agent/` | `.agent/skills/` | Usually none | `.agent/workflows/` | +| 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/` (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/` | `.agents/skills/` | `.zcode/cli/agents/` | pull-based prelude (no hooks) | + +## Capability Groups + +### Trellis Sub-Agent Support + +These platforms usually have `trellis-research`, `trellis-implement`, and `trellis-check` files: + +- Claude Code +- Cursor +- OpenCode +- Codex +- Kiro +- Gemini CLI +- Qoder +- CodeBuddy +- GitHub Copilot +- Factory Droid +- Pi Agent +- Trae IDE +- Reasonix (delivered as skills with `runAs: subagent` under `.reasonix/skills/`, not as a separate `agents/` directory) +- ZCode + +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 +- 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. + +## Decision Rules When Modifying Platform Files + +1. User specified a platform: modify only that platform directory unless shared workflow/spec files must also change. +2. User says "all platforms should do this": synchronize equivalent entry points platform by platform; do not modify only one directory. +3. User only says "my AI": inspect the configuration directories that actually exist in the project and infer the current AI platform. +4. User wants project rules: prefer `.trellis/spec/` or a project-local skill. +5. User wants Trellis behavior: edit `.trellis/workflow.md` plus platform hooks/agents/skills/commands. + +## When Paths Differ + +Platform ecosystems change, and user projects may already be customized. If this table disagrees with local files, use the actual settings/config in the user project as authoritative: + +- Check the hook that settings registers. +- Check the script that a command/prompt/workflow points to. +- Judge behavior by the read rules currently written in the agent file. + +Do not delete a custom file just because it is not listed in this path table. diff --git a/.trae/skills/trellis-meta/references/platform-files/skills-and-commands.md b/.trae/skills/trellis-meta/references/platform-files/skills-and-commands.md new file mode 100644 index 0000000..4732c6a --- /dev/null +++ b/.trae/skills/trellis-meta/references/platform-files/skills-and-commands.md @@ -0,0 +1,85 @@ +# Skills, Commands, Prompts, And Workflows + +Skills and commands are textual entry points for user interaction with Trellis. Different platforms use different names, but their core purpose is the same: tell the AI how to enter the Trellis flow when the user expresses a certain intent. + +## Conceptual Differences + +| Type | Trigger mode | Best for | +| --- | --- | --- | +| skill | AI auto-match or explicit user mention | Long-term capabilities, workflow rules, modification guides. | +| command | Explicit user invocation | Clear operation entry points such as continue and finish-work. | +| prompt | Explicit user invocation or platform selection | Similar to command, but in a platform prompt format. | +| workflow | Explicit user selection or platform auto-match | Guides the main session when no sub-agent/hook exists. | + +Trellis workflow skills usually share one semantic set: brainstorm, before-dev, check, update-spec, break-loop. Multi-file built-in skills such as `trellis-meta` use layered references. + +## Common Paths + +| Platform | Common entries | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| Kilo | `.kilocode/skills/`, `.kilocode/workflows/` | +| Kiro | `.kiro/skills/` | +| Gemini CLI | `.agents/skills/`, `.gemini/commands/` | +| Antigravity | `.agent/skills/`, `.agent/workflows/` | +| 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/` | +| Reasonix | `.reasonix/skills/` | +| ZCode | `.agents/skills/`, `.zcode/commands/` | + +In a user project, use the files actually generated by init as authoritative. + +## Skill Structure + +A common skill is a directory: + +```text +trellis-meta/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should tell the AI: + +- When to use this skill. +- Which reference to read first for the current task. +- What not to do. + +References hold longer explanations so the entry file does not contain everything. + +## Command/Prompt/Workflow Structure + +Commands, prompts, and workflows are usually single files. Their content should include: + +- When to use it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +They should not store task state; task state belongs in `.trellis/tasks/` and `.trellis/.runtime/`. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Change AI auto-trigger rules | The corresponding skill's frontmatter description. | +| Change user command behavior | The corresponding command/prompt/workflow file. | +| Add a project-local skill | Platform skill directory, or shared `.agents/skills/`. | +| Let multiple platforms share one capability | Write equivalent skills in each platform skill directory, or use the `.agents/skills/` shared layer on platforms that support it. | +| Change finish/continue entry points | Platform commands/prompts/workflows. | + +## Modification Principles + +1. **Keep entry files short; references carry long content**. This matters especially for multi-file skills like `trellis-meta`. +2. **Make trigger descriptions specific**. A description that is too broad can mis-trigger; one that is too narrow may not trigger. +3. **Keep the same semantics consistent across platforms**. File formats can differ, but behavior descriptions should match. +4. **Put project-specific capabilities in local skills**. Do not put team-private flows into public `trellis-meta`. + +If the user only wants local AI to know one more project rule, usually create a project-local skill or update `.trellis/spec/` instead of changing a Trellis built-in workflow skill. diff --git a/.trae/skills/trellis-session-insight/SKILL.md b/.trae/skills/trellis-session-insight/SKILL.md new file mode 100644 index 0000000..8e2cbb7 --- /dev/null +++ b/.trae/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, and Pi Agent conversation logs (the JSONL files each platform stores under `~/.claude/projects/`, `~/.codex/sessions/`, and `~/.pi/agent/sessions/`) and lets you list, search, slice by Trellis task boundaries, and dump cleaned dialogue from them. 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/.trae/skills/trellis-session-insight/references/cli-quick-reference.md b/.trae/skills/trellis-session-insight/references/cli-quick-reference.md new file mode 100644 index 0000000..78540f2 --- /dev/null +++ b/.trae/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/.trae/skills/trellis-session-insight/references/triggering-patterns.md b/.trae/skills/trellis-session-insight/references/triggering-patterns.md new file mode 100644 index 0000000..66021ca --- /dev/null +++ b/.trae/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/.trae/skills/trellis-spec-bootstrap/SKILL.md b/.trae/skills/trellis-spec-bootstrap/SKILL.md new file mode 100644 index 0000000..e1650df --- /dev/null +++ b/.trae/skills/trellis-spec-bootstrap/SKILL.md @@ -0,0 +1,41 @@ +--- +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 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. + +## Workflow + +1. Confirm Trellis is initialized and inspect the current `.trellis/spec/` tree. +2. Analyze the repository architecture with the best available tools: GitNexus, ABCoder, language tooling, and direct source reads. +3. Decompose the spec work by package and layer only when that reflects the actual codebase. +4. Fill or reshape the spec files with concrete patterns, file paths, examples, and anti-patterns from the project. +5. Verify that the final specs are internally consistent and contain no template placeholders. + +## Reference Routing + +| Need | Read | +|------|------| +| Repository architecture analysis | [references/repository-analysis.md](references/repository-analysis.md) | +| Spec work decomposition and task planning | [references/spec-task-planning.md](references/spec-task-planning.md) | +| Writing high-signal Trellis spec files | [references/spec-writing.md](references/spec-writing.md) | +| GitNexus and ABCoder MCP setup | [references/mcp-setup.md](references/mcp-setup.md) | + +## Operating Rules + +- Treat templates as starting points, not contracts. Delete, rename, split, or add spec files when the repository calls for it. +- Prefer source-backed rules over generic advice. Every important recommendation should point at a real file or repeated local pattern. +- Keep execution single-owner by default. Optional helper agents are an implementation detail, not a requirement or user-visible dependency. +- Do not write platform-specific instructions unless the target project already standardizes on that platform. +- Do not leave placeholder text, empty headings, or copied boilerplate in `.trellis/spec/`. + +## Done Criteria + +- `.trellis/spec/` describes the project as it exists now. +- Each relevant package or layer has practical coding guidance with real examples. +- Non-applicable template sections are removed. +- `index.md` files match the final spec file set. +- Any required setup or analysis assumptions are documented in the relevant spec or task notes. diff --git a/.trae/skills/trellis-spec-bootstrap/references/mcp-setup.md b/.trae/skills/trellis-spec-bootstrap/references/mcp-setup.md new file mode 100644 index 0000000..629fcbd --- /dev/null +++ b/.trae/skills/trellis-spec-bootstrap/references/mcp-setup.md @@ -0,0 +1,90 @@ +# MCP Setup + +GitNexus and ABCoder are recommended when bootstrapping Trellis specs because they expose architecture and AST context to the agent. They are tool choices, not platform requirements. Configure them through whatever MCP mechanism your agent host provides. + +## GitNexus + +GitNexus builds a code knowledge graph from the repository. Use it for module boundaries, execution flows, dependency relationships, blast radius, and graph queries. + +### Install and Index + +```bash +# Run from the repository root. +npx gitnexus analyze + +# Check index status. +npx gitnexus status + +# Re-index after code changes when the analysis is stale. +npx gitnexus analyze +``` + +The index is written to `.gitnexus/`. Keep embeddings only if the project already uses them; otherwise a normal index is enough for spec bootstrapping. + +### MCP Server Command + +Use this server command in the host's MCP configuration: + +```bash +npx -y gitnexus mcp +``` + +### Useful Tools + +| Tool | Purpose | +|------|---------| +| `gitnexus_query` | Find execution flows and functional areas by concept | +| `gitnexus_context` | Inspect callers, callees, references, and process participation for a symbol | +| `gitnexus_impact` | Understand blast radius before changing a symbol | +| `gitnexus_detect_changes` | Check changed symbols and affected flows before finishing | +| `gitnexus_cypher` | Run direct graph queries | +| `gitnexus_list_repos` | List indexed repositories | + +## ABCoder + +ABCoder parses code into UniAST and gives precise package, file, and node-level structure. Use it for signatures, type shapes, implementations, dependencies, and reverse references. + +### Install + +```bash +go install github.com/cloudwego/abcoder@latest +abcoder --help +``` + +### Parse Repositories + +```bash +abcoder parse /absolute/path/to/package \ + --lang typescript \ + --name package-name \ + --output ~/abcoder-asts +``` + +For monorepos, parse each package with a stable `--name` so task notes can reference the same repository names. + +### MCP Server Command + +Use this server command in the host's MCP configuration: + +```bash +abcoder mcp ~/abcoder-asts +``` + +### Useful Tools + +| Tool | Layer | Purpose | +|------|-------|---------| +| `list_repos` | 1 | List parsed repositories | +| `get_repo_structure` | 2 | Inspect packages and files | +| `get_package_structure` | 3 | Inspect nodes within a package | +| `get_file_structure` | 3 | Inspect functions, classes, types, and signatures in a file | +| `get_ast_node` | 4 | Retrieve code, dependencies, references, and implementations | + +## Verification + +After configuration, verify from the agent host that both MCP servers are visible. Then run one simple query against each server before starting the spec writing pass. + +```bash +ls .gitnexus/meta.json +ls ~/abcoder-asts/*.json +``` diff --git a/.trae/skills/trellis-spec-bootstrap/references/repository-analysis.md b/.trae/skills/trellis-spec-bootstrap/references/repository-analysis.md new file mode 100644 index 0000000..1309d29 --- /dev/null +++ b/.trae/skills/trellis-spec-bootstrap/references/repository-analysis.md @@ -0,0 +1,59 @@ +# Repository Analysis + +The goal is to discover the project's real architecture before writing rules. Do not start from generic spec templates and fill blanks. Start from the code, then let the spec structure follow. + +## Analysis Order + +1. Read the existing `.trellis/spec/` tree and note which files are templates, outdated, or already project-specific. +2. Inspect package manifests, build scripts, workspace config, and top-level documentation to identify packages and runtime layers. +3. Use GitNexus for execution flows, module clusters, dependency hubs, and impact-sensitive areas. +4. Use ABCoder or language-native tooling for exact signatures, types, class boundaries, and implementation examples. +5. Read representative source and test files directly before turning any finding into a spec rule. + +## What To Capture + +| Area | Questions | +|------|-----------| +| Package boundaries | What does each package own? What imports cross boundaries? | +| Runtime layers | Which code is CLI, backend, frontend, worker, shared library, test-only, or tooling? | +| Core abstractions | Which types, services, stores, commands, routes, or adapters define the system shape? | +| Data flow | Where does user input enter, how is it validated, and where does state persist? | +| Error handling | How are failures represented, logged, surfaced, and tested? | +| Configuration | Where do defaults, environment config, generated files, and templates live? | +| Tests | Which test styles are trusted examples for new work? | + +## GitNexus Usage + +Start broad, then inspect specific symbols: + +```text +gitnexus_query({query: "CLI command execution flow"}) +gitnexus_query({query: "template generation and migration"}) +gitnexus_context({name: "SymbolName"}) +gitnexus_cypher({query: "MATCH (n)-[r]->(m) RETURN n.name, type(r), m.name LIMIT 30"}) +``` + +Use GitNexus results to find important files and flows. Do not quote graph output as the final authority until you have checked the relevant source files. + +## ABCoder Usage + +Use ABCoder when the spec needs exact code shapes: + +```text +list_repos() +get_repo_structure({repo_name: "package-name"}) +get_file_structure({repo_name: "package-name", file_path: "src/example.ts"}) +get_ast_node({repo_name: "package-name", node_ids: [{mod_path: "...", pkg_path: "...", name: "SymbolName"}]}) +``` + +ABCoder is most valuable for documenting constructor patterns, function signatures, type contracts, and reference chains. + +## Analysis Notes + +Keep short notes while analyzing. The notes should include: + +- Package or layer name. +- Files that define the local pattern. +- Rules the spec should teach. +- Anti-patterns found in old code, comments, tests, or migration paths. +- Spec files that should be created, deleted, renamed, or merged. diff --git a/.trae/skills/trellis-spec-bootstrap/references/spec-task-planning.md b/.trae/skills/trellis-spec-bootstrap/references/spec-task-planning.md new file mode 100644 index 0000000..dca2687 --- /dev/null +++ b/.trae/skills/trellis-spec-bootstrap/references/spec-task-planning.md @@ -0,0 +1,61 @@ +# Spec Task Planning + +Use a single agent as the default execution model. The agent may create Trellis tasks for traceability, but the skill should not require a specific platform, CLI, or parallel worker model. + +## Decomposition + +Create spec work units around real ownership boundaries: + +- One package when a package has its own conventions. +- One layer when the same package has distinct frontend, backend, CLI, worker, or shared-library rules. +- One cross-cutting guide when a pattern spans packages and is not owned by one layer. + +Avoid artificial decomposition. A small library usually needs one focused spec pass, not several tasks. + +## Task Shape + +When a Trellis task is useful, write a concise PRD with these sections: + +```markdown +# Fill <package-or-layer> Trellis Specs + +## Goal +Write project-specific `.trellis/spec/` guidance for <scope>. + +## Scope +- Spec directory: +- Source directories to inspect: +- Tests to inspect: +- Out of scope: + +## Architecture Context +Summarize the concrete findings from repository analysis. + +## Files To Create Or Update +- `.trellis/spec/.../index.md` +- `.trellis/spec/.../<topic>.md` + +## Rules +- Adapt the spec file set to the real codebase. +- Use real source examples with file paths. +- Remove template-only sections that do not apply. +- Do not modify product source code unless the task explicitly asks for it. + +## Acceptance Criteria +- [ ] Specs contain concrete examples and anti-patterns from the repository. +- [ ] No placeholder text remains. +- [ ] Index files match the final spec files. +- [ ] Claims are backed by source files, tests, or project docs. +``` + +## Optional Helper Agents + +If the host supports subagents, helpers can inspect independent packages or run verification. They are optional. The main agent still owns integration and final quality. + +Helper tasks must have clear ownership: + +- Read-only research tasks may inspect any source needed for the assigned scope. +- Write tasks should own disjoint spec directories. +- Verification tasks should check placeholder removal, broken links, and consistency. + +Do not encode helper-agent names, vendor-specific commands, or platform-specific routing in the skill. Put only the required work and acceptance criteria in the task. diff --git a/.trae/skills/trellis-spec-bootstrap/references/spec-writing.md b/.trae/skills/trellis-spec-bootstrap/references/spec-writing.md new file mode 100644 index 0000000..6bc7dec --- /dev/null +++ b/.trae/skills/trellis-spec-bootstrap/references/spec-writing.md @@ -0,0 +1,70 @@ +# Spec Writing + +Trellis specs are coding guidance for future agents. They should explain how to work in this repository, not how a generic project might be organized. + +## Write From Evidence + +Each important rule should be backed by one of these: + +- A source file that demonstrates the preferred pattern. +- A test file that shows expected behavior. +- A project document that defines the convention. +- A repeated pattern across multiple files. + +Use short snippets only when they make the rule clearer. Prefer linking to the file path and naming the symbol or behavior. + +## File Structure + +Keep the spec tree aligned with the project: + +- Keep `index.md` as the navigation file for the spec directory. +- Split topics when developers would look for them independently. +- Merge topics when separate files would repeat the same rule. +- Delete template files that do not apply. +- Add new files for important local patterns the template missed. + +## Content Standards + +Good spec sections include: + +- When the rule applies. +- The local pattern to follow. +- The source or test files that prove the pattern. +- Common mistakes or anti-patterns. +- Verification commands or checks when they are specific and reliable. + +Avoid: + +- Placeholder prose. +- Generic framework advice. +- Tool instructions that only work in one agent host. +- Long copied code blocks. +- Rules based on a single accidental implementation detail. + +## Example Shape + +```markdown +## Command Handlers + +Command handlers should keep argument parsing, validation, and side effects separate. The local pattern is: + +- Parse CLI flags at the command boundary. +- Convert raw inputs into typed task options before invoking core logic. +- Keep filesystem writes in the command or service layer, not in template helpers. + +Reference files: +- `packages/cli/src/commands/example.ts` +- `packages/cli/test/commands/example.test.ts` + +Avoid passing raw `process.argv` or unvalidated config objects into shared helpers. +``` + +## Final Pass + +Before finishing: + +```bash +grep -R "To be filled\\|TODO: fill\\|placeholder" .trellis/spec +``` + +Also check links, index files, and whether any spec still describes a template rather than this repository. diff --git a/.trae/skills/trellis-update-spec/SKILL.md b/.trae/skills/trellis-update-spec/SKILL.md new file mode 100644 index 0000000..2b828a5 --- /dev/null +++ b/.trae/skills/trellis-update-spec/SKILL.md @@ -0,0 +1,356 @@ +--- +name: trellis-update-spec +description: "Captures executable contracts and coding conventions into .trellis/spec/ documents. Use when learning something valuable from debugging, implementing, or discussion that should be preserved for future sessions." +--- + +# Update Code-Spec - Capture Executable Contracts + +When you learn something valuable (from debugging, implementing, or discussion), use this to update the relevant code-spec documents. + +**Timing**: After completing a task, fixing a bug, or discovering a new pattern + +--- + +## Code-Spec First Rule (CRITICAL) + +In this project, "spec" for implementation work means **code-spec**: +- Executable contracts (not principle-only text) +- Concrete signatures, payload fields, env keys, and boundary behavior +- Testable validation/error behavior + +If the change touches infra or cross-layer contracts, code-spec depth is mandatory. + +### Mandatory Triggers + +Apply code-spec depth when the change includes any of: +- New/changed command or API signature +- Cross-layer request/response contract change +- Database schema/migration change +- Infra integration (storage, queue, cache, secrets, env wiring) + +### Mandatory Output (7 Sections) + +For triggered tasks, include all sections below: +1. Scope / Trigger +2. Signatures (command/API/DB) +3. Contracts (request/response/env) +4. Validation & Error Matrix +5. Good/Base/Bad Cases +6. Tests Required (with assertion points) +7. Wrong vs Correct (at least one pair) + +--- + +## When to Update Code-Specs + +| Trigger | Example | Target Spec | +|---------|---------|-------------| +| **Implemented a feature** | Added a new integration or module | Relevant spec file | +| **Made a design decision** | Chose extensibility pattern over simplicity | Relevant spec + "Design Decisions" section | +| **Fixed a bug** | Found a subtle issue with error handling | Relevant spec (e.g., error-handling docs) | +| **Discovered a pattern** | Found a better way to structure code | Relevant spec file | +| **Hit a gotcha** | Learned that X must be done before Y | Relevant spec + "Common Mistakes" section | +| **Established a convention** | Team agreed on naming pattern | Quality guidelines | +| **New thinking trigger** | "Don't forget to check X before doing Y" | `guides/*.md` (as a checklist item) | + +**Key Insight**: Code-spec updates are NOT just for problems. Every feature implementation contains design decisions and contracts that future AI/developers need to execute safely. + +--- + +## Spec Structure Overview + +``` +.trellis/spec/ +├── <layer>/ # Per-layer coding standards (e.g., backend/, frontend/, api/) +│ ├── index.md # Overview and links +│ └── *.md # Topic-specific guidelines +└── guides/ # Thinking checklists (NOT coding specs!) + ├── index.md # Guide index + └── *.md # Topic-specific guides +``` + +### CRITICAL: Code-Spec vs Guide - Know the Difference + +| Type | Location | Purpose | Content Style | +|------|----------|---------|---------------| +| **Code-Spec** | `<layer>/*.md` | Tell AI "how to implement safely" | Signatures, contracts, matrices, cases, test points | +| **Guide** | `guides/*.md` | Help AI "what to think about" | Checklists, questions, pointers to specs | + +**Decision Rule**: Ask yourself: + +- "This is **how to write** the code" → Put in a spec layer directory +- "This is **what to consider** before writing" → Put in `guides/` + +**Example**: + +| Learning | Wrong Location | Correct Location | +|----------|----------------|------------------| +| "Use API X not API Y for this task" | ❌ `guides/` (too specific for a thinking guide) | ✅ Relevant spec file (concrete convention) | +| "Remember to check X when doing Y" | ❌ Spec file (too abstract for a spec) | ✅ `guides/` (thinking checklist) | + +**Guides should be short checklists that point to specs**, not duplicate the detailed rules. + +--- + +## Update Process + +### Step 1: Identify What You Learned + +Answer these questions: + +1. **What did you learn?** (Be specific) +2. **Why is it important?** (What problem does it prevent?) +3. **Where does it belong?** (Which spec file?) + +### Step 2: Classify the Update Type + +| Type | Description | Action | +|------|-------------|--------| +| **Design Decision** | Why we chose approach X over Y | Add to "Design Decisions" section | +| **Project Convention** | How we do X in this project | Add to relevant section with examples | +| **New Pattern** | A reusable approach discovered | Add to "Patterns" section | +| **Forbidden Pattern** | Something that causes problems | Add to "Anti-patterns" or "Don't" section | +| **Common Mistake** | Easy-to-make error | Add to "Common Mistakes" section | +| **Convention** | Agreed-upon standard | Add to relevant section | +| **Gotcha** | Non-obvious behavior | Add warning callout | + +### Step 3: Read the Target Code-Spec + +Before editing, read the current code-spec to: +- Understand existing structure +- Avoid duplicating content +- Find the right section for your update + +```bash +cat .trellis/spec/<category>/<file>.md +``` + +### Step 4: Make the Update + +Follow these principles: + +1. **Be Specific**: Include concrete examples, not just abstract rules +2. **Explain Why**: State the problem this prevents +3. **Show Contracts**: Add signatures, payload fields, and error behavior +4. **Show Code**: Add code snippets for key patterns +5. **Keep it Short**: One concept per section + +### Step 5: Update the Index (if needed) + +If you added a new section or the code-spec status changed, update the category's `index.md`. + +--- + +## Update Templates + +### Mandatory Template for Infra/Cross-Layer Work + +```markdown +## Scenario: <name> + +### 1. Scope / Trigger +- Trigger: <why this requires code-spec depth> + +### 2. Signatures +- Backend command/API/DB signature(s) + +### 3. Contracts +- Request fields (name, type, constraints) +- Response fields (name, type, constraints) +- Environment keys (required/optional) + +### 4. Validation & Error Matrix +- <condition> -> <error> + +### 5. Good/Base/Bad Cases +- Good: ... +- Base: ... +- Bad: ... + +### 6. Tests Required +- Unit/Integration/E2E with assertion points + +### 7. Wrong vs Correct +#### Wrong +... +#### Correct +... +``` + +### Adding a Design Decision + +```markdown +### Design Decision: [Decision Name] + +**Context**: What problem were we solving? + +**Options Considered**: +1. Option A - brief description +2. Option B - brief description + +**Decision**: We chose Option X because... + +**Example**: +\`\`\`typescript +// How it's implemented +code example +\`\`\` + +**Extensibility**: How to extend this in the future... +``` + +### Adding a Project Convention + +```markdown +### Convention: [Convention Name] + +**What**: Brief description of the convention. + +**Why**: Why we do it this way in this project. + +**Example**: +\`\`\`typescript +// How to follow this convention +code example +\`\`\` + +**Related**: Links to related conventions or specs. +``` + +### Adding a New Pattern + +```markdown +### Pattern Name + +**Problem**: What problem does this solve? + +**Solution**: Brief description of the approach. + +**Example**: +\`\`\` +// Good +code example + +// Bad +code example +\`\`\` + +**Why**: Explanation of why this works better. +``` + +### Adding a Forbidden Pattern + +```markdown +### Don't: Pattern Name + +**Problem**: +\`\`\` +// Don't do this +bad code example +\`\`\` + +**Why it's bad**: Explanation of the issue. + +**Instead**: +\`\`\` +// Do this instead +good code example +\`\`\` +``` + +### Adding a Common Mistake + +```markdown +### Common Mistake: Description + +**Symptom**: What goes wrong + +**Cause**: Why this happens + +**Fix**: How to correct it + +**Prevention**: How to avoid it in the future +``` + +### Adding a Gotcha + +```markdown +> **Warning**: Brief description of the non-obvious behavior. +> +> Details about when this happens and how to handle it. +``` + +--- + +## Interactive Mode + +If you're unsure what to update, answer these prompts: + +1. **What did you just finish?** + - [ ] Fixed a bug + - [ ] Implemented a feature + - [ ] Refactored code + - [ ] Had a discussion about approach + +2. **What did you learn or decide?** + - Design decision (why X over Y) + - Project convention (how we do X) + - Non-obvious behavior (gotcha) + - Better approach (pattern) + +3. **Would future AI/developers need to know this?** + - To understand how the code works → Yes, update spec + - To maintain or extend the feature → Yes, update spec + - To avoid repeating mistakes → Yes, update spec + - Purely one-off implementation detail → Maybe skip + +4. **Which area does it relate to?** + - [ ] Backend code + - [ ] Frontend code + - [ ] Cross-layer data flow + - [ ] Code organization/reuse + - [ ] Quality/testing + +--- + +## Quality Checklist + +Before finishing your code-spec update: + +- [ ] Is the content specific and actionable? +- [ ] Did you include a code example? +- [ ] Did you explain WHY, not just WHAT? +- [ ] Did you include executable signatures/contracts? +- [ ] Did you include validation and error matrix? +- [ ] Did you include Good/Base/Bad cases? +- [ ] Did you include required tests with assertion points? +- [ ] Is it in the right code-spec file? +- [ ] Does it duplicate existing content? +- [ ] Would a new team member understand it? + +--- + +## Relationship to Other Commands + +``` +Development Flow: + Learn something → /trellis-update-spec → Knowledge captured + ↑ ↓ + /trellis-break-loop ←──────────────────── Future sessions benefit + (deep bug analysis) +``` + +- `/trellis-break-loop` - Analyzes bugs deeply, often reveals spec updates needed +- `/trellis-update-spec` - Actually makes the updates +- `/trellis-finish-work` - Reminds you to check if specs need updates + +--- + +## Core Philosophy + +> **Code-specs are living documents. Every debugging session, every "aha moment" is an opportunity to make the implementation contract clearer.** + +The goal is **institutional memory**: +- What one person learns, everyone benefits from +- What AI learns in one session, persists to future sessions +- Mistakes become documented guardrails diff --git a/.trellis/.gitignore b/.trellis/.gitignore new file mode 100644 index 0000000..5a991ea --- /dev/null +++ b/.trellis/.gitignore @@ -0,0 +1,32 @@ +# Developer identity (local only) +.developer + +# Current task pointer (each dev works on different task) +.current-task + +# Session/window scoped runtime state +.runtime/ + +# Ralph Loop state file +.ralph-state.json + +# Agent runtime files +.agents/ +.agent-log +.session-id + +# Task directory runtime files +.plan-log + +# Atomic update temp files +*.tmp + +# Update backup directories +.backup-* + +# Conflict resolution temp files +*.new + +# Python cache +**/__pycache__/ +**/*.pyc diff --git a/.trellis/.template-hashes.json b/.trellis/.template-hashes.json new file mode 100644 index 0000000..552599d --- /dev/null +++ b/.trellis/.template-hashes.json @@ -0,0 +1,333 @@ +{ + "__version": 2, + "hashes": { + ".claude/agents/trellis-check.md": "9e48342243f311d55386f8fb42933945e87aba73d5ade547133aa98345a06128", + ".claude/agents/trellis-implement.md": "73b56b3047c0e852382c4630aa181fb847d1e5ea1f63459dac4bf728f43fd097", + ".claude/agents/trellis-research.md": "add4aa4259ded425b04ec992802c646490352bb4eb3730a7ab450beea87d4faa", + ".claude/settings.json": "1a65892b2b161910468970ab30ebc3f8216241640f75fccbe4be5552c58a2752", + ".claude/hooks/inject-subagent-context.py": "c8ea04062990530dffb26c4d1efa3e6887e042d3086da2f2325b491c9d544931", + ".claude/hooks/inject-workflow-state.py": "552d4ee4ca059337856c587fde1aaf79364444d0465689faa88c43797969b7de", + ".claude/hooks/session-start.py": "688e9d5c2273575b2c1cc8db17fa39da7b1798542a67f65286d97cdbdc347995", + ".claude/hooks/statusline.py": "019130b79f062192e149b6b09fda2103ee2aaac9e2f0ccba0efd6fbc5e156cf3", + ".claude/commands/trellis/continue.md": "6c34c41824f8eff4b2df792e032e0c36787b59f8a8879b0338347073f76ad52c", + ".claude/commands/trellis/finish-work.md": "d6aa570ab684f57e4845de2d84a1ff6d9f0908e04c5a56e14fd70ae739c369fc", + ".claude/skills/trellis-before-dev/SKILL.md": "859894f2e8258cbfa142d710363433761c54984c69f7ed7bd44512fb4eb165cd", + ".claude/skills/trellis-brainstorm/SKILL.md": "3bbfb6506af3c318c6f9b3c280517ce93a02e40e27535de7ff5fc029d3027860", + ".claude/skills/trellis-break-loop/SKILL.md": "f5a93699832f29dee443b53c135a7459519b371f689af191d15c29e8ee5c7bde", + ".claude/skills/trellis-check/SKILL.md": "b21ff04b7680ebacb8c5ecbc48a22d627eb13e2b47fceb78c8ced0b43b60b282", + ".claude/skills/trellis-update-spec/SKILL.md": "d975db7af166578488958751ae2c56edb827a68bddb569aa27acc3453f64e610", + ".claude/skills/trellis-channel/references/command-reference.md": "c4df3d940d89814310bfdaded20e84dd1d6b96786f6772688de7a123338d3c8e", + ".claude/skills/trellis-channel/references/forum.md": "407db39e8ebde47cea8637e3069bdb3d76ba42b43ed1d08ba97a56f26dbcfcb4", + ".claude/skills/trellis-channel/references/progress-debugging.md": "93b021fd8d5d3d4ee54952c40424ab91ca57c2ab8032bfbfc3b5c5e22f1464cd", + ".claude/skills/trellis-channel/references/workers.md": "d4481b2858bca82050cb8d131916da8a2ccc3857f199cdb9ced8bb41d592e2a7", + ".claude/skills/trellis-channel/references/workflows.md": "0782d602efde6e94a7d7d7950fd605eedc9a1f5a523908284e76b628fd618fd4", + ".claude/skills/trellis-channel/SKILL.md": "ff84801c511a736da018b4b1149348ebc9a16b7fc18d905ec10cfa7acbf269a3", + ".claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md": "86009ccb5d0373f399582da0bc570c4e5c6053c3c764857424ff93384f0e04e5", + ".claude/skills/trellis-meta/references/customize-local/change-agents.md": "3eef0d9b9cf875f121c1a38afb8eee6d3cd3894127db601ae5a7760fa5b61575", + ".claude/skills/trellis-meta/references/customize-local/change-context-loading.md": "350d319dc1ab99609ddbf52cf8c06c71bd97ba1a29ce2eac8b97d0bb192938bf", + ".claude/skills/trellis-meta/references/customize-local/change-hooks.md": "4c18b134f05d5ba1609c517ec94c97c9442b5f0670aa2211c487c31ed2a4f358", + ".claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "a6994e2418cdf5bcad10b4236b02741179ae794bc4fdd811a64f443293b69268", + ".claude/skills/trellis-meta/references/customize-local/change-spec-structure.md": "31eccaad7097d96e66a45c1b4caea1ba4f2e54b7814184c3ebf82c87dafc4841", + ".claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md": "60ff9efb93604b87a461a4af30322d76750402a51e40f31531a7ff88d309996d", + ".claude/skills/trellis-meta/references/customize-local/change-workflow.md": "43fa780a2ca580de121b10893d49b99f978873deebbf45008c466e5ac6651519", + ".claude/skills/trellis-meta/references/customize-local/overview.md": "ce8f09e9f93ce9a48500763fb3a4db2b3908a5fbf4f985ab71dacebb404cf8f4", + ".claude/skills/trellis-meta/references/local-architecture/bundled-skills.md": "bac739b042d7a12dae5d14d85cbc312e56ca03f571d2ba7a25b20641ddaefc3f", + ".claude/skills/trellis-meta/references/local-architecture/context-injection.md": "8497289bf333b3aa456f317039d1239b7ece79254aa0eb62cfc647714c866084", + ".claude/skills/trellis-meta/references/local-architecture/generated-files.md": "1128d45b2e8c011c247bcf3a3ca1184ca4951fb34a0802c50bf3fecbd7e4de55", + ".claude/skills/trellis-meta/references/local-architecture/multi-agent-channel.md": "56e5070474aeca872e2d70c46feea5aaafecd3d3ec052c3f7b1877358dca62e9", + ".claude/skills/trellis-meta/references/local-architecture/overview.md": "50638fd9eaaba2e0edf2f2a84d920578d5b2fb7934031b174e417d3dc510b2a6", + ".claude/skills/trellis-meta/references/local-architecture/spec-system.md": "b8d8a6a0888b44a232c8f50161b9e20e903cf621ad7be4021715ab6fab226f47", + ".claude/skills/trellis-meta/references/local-architecture/task-system.md": "2b561d49c390f7d0db5391912946133be4bf73189231e2b8cc9afa1c5ac6165a", + ".claude/skills/trellis-meta/references/local-architecture/workflow.md": "cfcdc6e4468a5d9c816e929fcca01640cd41cfdaaa4824118b40a8e460c927b6", + ".claude/skills/trellis-meta/references/local-architecture/workspace-memory.md": "e6427b46aba744563c2444b30df4043cd856561b7709ec2dece26095416421fd", + ".claude/skills/trellis-meta/references/platform-files/agents.md": "ce1a22c2b3ff0bff68755b5663e1f3b63ef2f1658cff43e511cee9013537c698", + ".claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "acdeba5866ccbe0e68fffc847e77e726cf79f579898e61312f6a8d62b48380ff", + ".claude/skills/trellis-meta/references/platform-files/overview.md": "821a2718b38e17439c2bb487a6dd3a95c8138e83dbda60b142a00ff80178dc3c", + ".claude/skills/trellis-meta/references/platform-files/platform-map.md": "59e13cdaa2077149b5d82ad25fa5dd821195be30bdc43d853619c4702d334a6e", + ".claude/skills/trellis-meta/references/platform-files/skills-and-commands.md": "5732bb0a7d384a91abd0c103372f3865e0b186e75185f35e0e91b00a6b6bf5a3", + ".claude/skills/trellis-meta/SKILL.md": "a190ecbde70a658a5b5c3e2558783e99c9afc6f2e5cdecdac8f11c06dd42065e", + ".claude/skills/trellis-session-insight/references/cli-quick-reference.md": "c520353fe3fc00b9702ed4f780647c8fbd6d342e66527a03647f533a9fe09779", + ".claude/skills/trellis-session-insight/references/triggering-patterns.md": "121ecd23be83d1567e8ce15c366a81073d7a2b1d3ad616fce235c07ca1f1cc20", + ".claude/skills/trellis-session-insight/SKILL.md": "f6ad80eaca21b8fa63d31ae26b612bb2bb69dd04469603828509a8c5a0463ff1", + ".claude/skills/trellis-spec-bootstrap/references/mcp-setup.md": "df542fc8f279edd38046d26a7c8151804b708f57b24d4aa2733cea587a88c65e", + ".claude/skills/trellis-spec-bootstrap/references/repository-analysis.md": "0dae98d774f6e34559b9f3442888ac43e3a8af110c37cbefc49ce256986858b6", + ".claude/skills/trellis-spec-bootstrap/references/spec-task-planning.md": "ef493d028c3b0807a8a534bb71fb92a68129f273db763ad27ceb464a522e799d", + ".claude/skills/trellis-spec-bootstrap/references/spec-writing.md": "e9800fe9ed4a4cd87062ea1829cf2caa8d170ec15e141678a6a30e74c497f47d", + ".claude/skills/trellis-spec-bootstrap/SKILL.md": "97bfa68c06cebb558eb4464bc1b81f7d2d56040d75baa8de1ee5ad90cca0196a", + ".cursor/commands/trellis-continue.md": "338f80aab71e576de25c736e3aa7ea73fc2d5b433ff03f90e78d8958870945bc", + ".cursor/commands/trellis-finish-work.md": "5a8a72fd87d009c15068bae75165af97f1b03e975b24f5dc3b901b01c6914160", + ".cursor/skills/trellis-before-dev/SKILL.md": "859894f2e8258cbfa142d710363433761c54984c69f7ed7bd44512fb4eb165cd", + ".cursor/skills/trellis-brainstorm/SKILL.md": "3bbfb6506af3c318c6f9b3c280517ce93a02e40e27535de7ff5fc029d3027860", + ".cursor/skills/trellis-break-loop/SKILL.md": "f5a93699832f29dee443b53c135a7459519b371f689af191d15c29e8ee5c7bde", + ".cursor/skills/trellis-check/SKILL.md": "b21ff04b7680ebacb8c5ecbc48a22d627eb13e2b47fceb78c8ced0b43b60b282", + ".cursor/skills/trellis-update-spec/SKILL.md": "cef32aec88db973a0a0272cf18b91d4585fe6ed6625e3de06851a5d3402f65d6", + ".cursor/skills/trellis-channel/references/command-reference.md": "c4df3d940d89814310bfdaded20e84dd1d6b96786f6772688de7a123338d3c8e", + ".cursor/skills/trellis-channel/references/forum.md": "407db39e8ebde47cea8637e3069bdb3d76ba42b43ed1d08ba97a56f26dbcfcb4", + ".cursor/skills/trellis-channel/references/progress-debugging.md": "93b021fd8d5d3d4ee54952c40424ab91ca57c2ab8032bfbfc3b5c5e22f1464cd", + ".cursor/skills/trellis-channel/references/workers.md": "d4481b2858bca82050cb8d131916da8a2ccc3857f199cdb9ced8bb41d592e2a7", + ".cursor/skills/trellis-channel/references/workflows.md": "0782d602efde6e94a7d7d7950fd605eedc9a1f5a523908284e76b628fd618fd4", + ".cursor/skills/trellis-channel/SKILL.md": "ff84801c511a736da018b4b1149348ebc9a16b7fc18d905ec10cfa7acbf269a3", + ".cursor/skills/trellis-meta/references/customize-local/add-project-local-conventions.md": "86009ccb5d0373f399582da0bc570c4e5c6053c3c764857424ff93384f0e04e5", + ".cursor/skills/trellis-meta/references/customize-local/change-agents.md": "3eef0d9b9cf875f121c1a38afb8eee6d3cd3894127db601ae5a7760fa5b61575", + ".cursor/skills/trellis-meta/references/customize-local/change-context-loading.md": "350d319dc1ab99609ddbf52cf8c06c71bd97ba1a29ce2eac8b97d0bb192938bf", + ".cursor/skills/trellis-meta/references/customize-local/change-hooks.md": "4c18b134f05d5ba1609c517ec94c97c9442b5f0670aa2211c487c31ed2a4f358", + ".cursor/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "a6994e2418cdf5bcad10b4236b02741179ae794bc4fdd811a64f443293b69268", + ".cursor/skills/trellis-meta/references/customize-local/change-spec-structure.md": "31eccaad7097d96e66a45c1b4caea1ba4f2e54b7814184c3ebf82c87dafc4841", + ".cursor/skills/trellis-meta/references/customize-local/change-task-lifecycle.md": "60ff9efb93604b87a461a4af30322d76750402a51e40f31531a7ff88d309996d", + ".cursor/skills/trellis-meta/references/customize-local/change-workflow.md": "43fa780a2ca580de121b10893d49b99f978873deebbf45008c466e5ac6651519", + ".cursor/skills/trellis-meta/references/customize-local/overview.md": "ce8f09e9f93ce9a48500763fb3a4db2b3908a5fbf4f985ab71dacebb404cf8f4", + ".cursor/skills/trellis-meta/references/local-architecture/bundled-skills.md": "bac739b042d7a12dae5d14d85cbc312e56ca03f571d2ba7a25b20641ddaefc3f", + ".cursor/skills/trellis-meta/references/local-architecture/context-injection.md": "8497289bf333b3aa456f317039d1239b7ece79254aa0eb62cfc647714c866084", + ".cursor/skills/trellis-meta/references/local-architecture/generated-files.md": "1128d45b2e8c011c247bcf3a3ca1184ca4951fb34a0802c50bf3fecbd7e4de55", + ".cursor/skills/trellis-meta/references/local-architecture/multi-agent-channel.md": "56e5070474aeca872e2d70c46feea5aaafecd3d3ec052c3f7b1877358dca62e9", + ".cursor/skills/trellis-meta/references/local-architecture/overview.md": "50638fd9eaaba2e0edf2f2a84d920578d5b2fb7934031b174e417d3dc510b2a6", + ".cursor/skills/trellis-meta/references/local-architecture/spec-system.md": "b8d8a6a0888b44a232c8f50161b9e20e903cf621ad7be4021715ab6fab226f47", + ".cursor/skills/trellis-meta/references/local-architecture/task-system.md": "2b561d49c390f7d0db5391912946133be4bf73189231e2b8cc9afa1c5ac6165a", + ".cursor/skills/trellis-meta/references/local-architecture/workflow.md": "cfcdc6e4468a5d9c816e929fcca01640cd41cfdaaa4824118b40a8e460c927b6", + ".cursor/skills/trellis-meta/references/local-architecture/workspace-memory.md": "e6427b46aba744563c2444b30df4043cd856561b7709ec2dece26095416421fd", + ".cursor/skills/trellis-meta/references/platform-files/agents.md": "ce1a22c2b3ff0bff68755b5663e1f3b63ef2f1658cff43e511cee9013537c698", + ".cursor/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "acdeba5866ccbe0e68fffc847e77e726cf79f579898e61312f6a8d62b48380ff", + ".cursor/skills/trellis-meta/references/platform-files/overview.md": "821a2718b38e17439c2bb487a6dd3a95c8138e83dbda60b142a00ff80178dc3c", + ".cursor/skills/trellis-meta/references/platform-files/platform-map.md": "59e13cdaa2077149b5d82ad25fa5dd821195be30bdc43d853619c4702d334a6e", + ".cursor/skills/trellis-meta/references/platform-files/skills-and-commands.md": "5732bb0a7d384a91abd0c103372f3865e0b186e75185f35e0e91b00a6b6bf5a3", + ".cursor/skills/trellis-meta/SKILL.md": "a190ecbde70a658a5b5c3e2558783e99c9afc6f2e5cdecdac8f11c06dd42065e", + ".cursor/skills/trellis-session-insight/references/cli-quick-reference.md": "c520353fe3fc00b9702ed4f780647c8fbd6d342e66527a03647f533a9fe09779", + ".cursor/skills/trellis-session-insight/references/triggering-patterns.md": "121ecd23be83d1567e8ce15c366a81073d7a2b1d3ad616fce235c07ca1f1cc20", + ".cursor/skills/trellis-session-insight/SKILL.md": "f6ad80eaca21b8fa63d31ae26b612bb2bb69dd04469603828509a8c5a0463ff1", + ".cursor/skills/trellis-spec-bootstrap/references/mcp-setup.md": "df542fc8f279edd38046d26a7c8151804b708f57b24d4aa2733cea587a88c65e", + ".cursor/skills/trellis-spec-bootstrap/references/repository-analysis.md": "0dae98d774f6e34559b9f3442888ac43e3a8af110c37cbefc49ce256986858b6", + ".cursor/skills/trellis-spec-bootstrap/references/spec-task-planning.md": "ef493d028c3b0807a8a534bb71fb92a68129f273db763ad27ceb464a522e799d", + ".cursor/skills/trellis-spec-bootstrap/references/spec-writing.md": "e9800fe9ed4a4cd87062ea1829cf2caa8d170ec15e141678a6a30e74c497f47d", + ".cursor/skills/trellis-spec-bootstrap/SKILL.md": "97bfa68c06cebb558eb4464bc1b81f7d2d56040d75baa8de1ee5ad90cca0196a", + ".cursor/agents/trellis-check.md": "e54d3de996bab4abd7653eef4d64c171638fbc2859e9b1fe4aae2483df72de99", + ".cursor/agents/trellis-implement.md": "636d55ebb56d9d3ea6ef9ec615c53bcaeab2b1afb3dc7e30ad51091efe5ab5ea", + ".cursor/agents/trellis-research.md": "1311b229a5d8c2c388ceb52460c3d837aa982c988aac39fb9ae30385072fbacf", + ".cursor/hooks/inject-shell-session-context.py": "28502dd7cb657fed92005c2e1c60334ce216545c40d4791b9433cbf779f83968", + ".cursor/hooks/inject-subagent-context.py": "c8ea04062990530dffb26c4d1efa3e6887e042d3086da2f2325b491c9d544931", + ".cursor/hooks/session-start.py": "688e9d5c2273575b2c1cc8db17fa39da7b1798542a67f65286d97cdbdc347995", + ".cursor/hooks.json": "c7a830671610c1d433c97b3cb880e317730862631fbe3fd76d052553c83f49b3", + ".agents/skills/trellis-continue/SKILL.md": "7723ccf49fbf19d8f086cacc7a080bd8be8db6fc70a32908b80f68efa318d7bf", + ".agents/skills/trellis-finish-work/SKILL.md": "161060fbcd44f787440d3a5c297a9f5223ea7774bb3021a50e376875a9ac5b2d", + ".agents/skills/trellis-start/SKILL.md": "79a5ba7a2aff3c72e06d7f4cd6942dc4f4f4092dd40f9c8e94f1838024a81e4d", + ".agents/skills/trellis-before-dev/SKILL.md": "859894f2e8258cbfa142d710363433761c54984c69f7ed7bd44512fb4eb165cd", + ".agents/skills/trellis-brainstorm/SKILL.md": "3bbfb6506af3c318c6f9b3c280517ce93a02e40e27535de7ff5fc029d3027860", + ".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-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/customize-local/add-project-local-conventions.md": "86009ccb5d0373f399582da0bc570c4e5c6053c3c764857424ff93384f0e04e5", + ".agents/skills/trellis-meta/references/customize-local/change-agents.md": "3eef0d9b9cf875f121c1a38afb8eee6d3cd3894127db601ae5a7760fa5b61575", + ".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": "a6994e2418cdf5bcad10b4236b02741179ae794bc4fdd811a64f443293b69268", + ".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": "43fa780a2ca580de121b10893d49b99f978873deebbf45008c466e5ac6651519", + ".agents/skills/trellis-meta/references/customize-local/overview.md": "ce8f09e9f93ce9a48500763fb3a4db2b3908a5fbf4f985ab71dacebb404cf8f4", + ".agents/skills/trellis-meta/references/local-architecture/bundled-skills.md": "bac739b042d7a12dae5d14d85cbc312e56ca03f571d2ba7a25b20641ddaefc3f", + ".agents/skills/trellis-meta/references/local-architecture/context-injection.md": "8497289bf333b3aa456f317039d1239b7ece79254aa0eb62cfc647714c866084", + ".agents/skills/trellis-meta/references/local-architecture/generated-files.md": "1128d45b2e8c011c247bcf3a3ca1184ca4951fb34a0802c50bf3fecbd7e4de55", + ".agents/skills/trellis-meta/references/local-architecture/multi-agent-channel.md": "56e5070474aeca872e2d70c46feea5aaafecd3d3ec052c3f7b1877358dca62e9", + ".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": "ce1a22c2b3ff0bff68755b5663e1f3b63ef2f1658cff43e511cee9013537c698", + ".agents/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "acdeba5866ccbe0e68fffc847e77e726cf79f579898e61312f6a8d62b48380ff", + ".agents/skills/trellis-meta/references/platform-files/overview.md": "821a2718b38e17439c2bb487a6dd3a95c8138e83dbda60b142a00ff80178dc3c", + ".agents/skills/trellis-meta/references/platform-files/platform-map.md": "59e13cdaa2077149b5d82ad25fa5dd821195be30bdc43d853619c4702d334a6e", + ".agents/skills/trellis-meta/references/platform-files/skills-and-commands.md": "5732bb0a7d384a91abd0c103372f3865e0b186e75185f35e0e91b00a6b6bf5a3", + ".agents/skills/trellis-meta/SKILL.md": "a190ecbde70a658a5b5c3e2558783e99c9afc6f2e5cdecdac8f11c06dd42065e", + ".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": "f6ad80eaca21b8fa63d31ae26b612bb2bb69dd04469603828509a8c5a0463ff1", + ".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/agents/trellis-check.toml": "372dbd32a68f156727fd9f5d755a184543db96013abfeaffb09974c78fd5b873", + ".codex/agents/trellis-implement.toml": "87dc719802b355c61607c85ae0ab3e42b2a374a578a9fde349c3ae412638f8b4", + ".codex/agents/trellis-research.toml": "5492f7f6ab8bdea975b0e853bf171b050f7ddf6c2079ac770ed912c48d815eae", + ".codex/hooks/session-start.py": "1c951ff35f490c5fbf576b4764ec190895df7c2a48e279fb20625209f51c321a", + ".codex/hooks/inject-workflow-state.py": "552d4ee4ca059337856c587fde1aaf79364444d0465689faa88c43797969b7de", + ".codex/hooks.json": "522ba3c488c100027783e52ecff84c0bd799852dd77ad3f1936e86db105f01d6", + ".codex/config.toml": "4224eb7df6802a623cb1bee522aed0a23ba6be862b90f1b597a313fc16864b06", + ".pi/prompts/trellis-start.md": "28af1eb6645d8b517cf705277d8405370b712926e6b01667d6698564002c6a9d", + ".pi/prompts/trellis-continue.md": "12c2f0288ff67af3368c0b577a50027a11a25fec1d32ed34282a4dc84be08f1c", + ".pi/prompts/trellis-finish-work.md": "5a8a72fd87d009c15068bae75165af97f1b03e975b24f5dc3b901b01c6914160", + ".pi/skills/trellis-before-dev/SKILL.md": "859894f2e8258cbfa142d710363433761c54984c69f7ed7bd44512fb4eb165cd", + ".pi/skills/trellis-brainstorm/SKILL.md": "3bbfb6506af3c318c6f9b3c280517ce93a02e40e27535de7ff5fc029d3027860", + ".pi/skills/trellis-break-loop/SKILL.md": "f5a93699832f29dee443b53c135a7459519b371f689af191d15c29e8ee5c7bde", + ".pi/skills/trellis-check/SKILL.md": "b21ff04b7680ebacb8c5ecbc48a22d627eb13e2b47fceb78c8ced0b43b60b282", + ".pi/skills/trellis-update-spec/SKILL.md": "cef32aec88db973a0a0272cf18b91d4585fe6ed6625e3de06851a5d3402f65d6", + ".pi/skills/trellis-channel/references/command-reference.md": "c4df3d940d89814310bfdaded20e84dd1d6b96786f6772688de7a123338d3c8e", + ".pi/skills/trellis-channel/references/forum.md": "407db39e8ebde47cea8637e3069bdb3d76ba42b43ed1d08ba97a56f26dbcfcb4", + ".pi/skills/trellis-channel/references/progress-debugging.md": "93b021fd8d5d3d4ee54952c40424ab91ca57c2ab8032bfbfc3b5c5e22f1464cd", + ".pi/skills/trellis-channel/references/workers.md": "d4481b2858bca82050cb8d131916da8a2ccc3857f199cdb9ced8bb41d592e2a7", + ".pi/skills/trellis-channel/references/workflows.md": "0782d602efde6e94a7d7d7950fd605eedc9a1f5a523908284e76b628fd618fd4", + ".pi/skills/trellis-channel/SKILL.md": "ff84801c511a736da018b4b1149348ebc9a16b7fc18d905ec10cfa7acbf269a3", + ".pi/skills/trellis-meta/references/customize-local/add-project-local-conventions.md": "86009ccb5d0373f399582da0bc570c4e5c6053c3c764857424ff93384f0e04e5", + ".pi/skills/trellis-meta/references/customize-local/change-agents.md": "3eef0d9b9cf875f121c1a38afb8eee6d3cd3894127db601ae5a7760fa5b61575", + ".pi/skills/trellis-meta/references/customize-local/change-context-loading.md": "350d319dc1ab99609ddbf52cf8c06c71bd97ba1a29ce2eac8b97d0bb192938bf", + ".pi/skills/trellis-meta/references/customize-local/change-hooks.md": "4c18b134f05d5ba1609c517ec94c97c9442b5f0670aa2211c487c31ed2a4f358", + ".pi/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "a6994e2418cdf5bcad10b4236b02741179ae794bc4fdd811a64f443293b69268", + ".pi/skills/trellis-meta/references/customize-local/change-spec-structure.md": "31eccaad7097d96e66a45c1b4caea1ba4f2e54b7814184c3ebf82c87dafc4841", + ".pi/skills/trellis-meta/references/customize-local/change-task-lifecycle.md": "60ff9efb93604b87a461a4af30322d76750402a51e40f31531a7ff88d309996d", + ".pi/skills/trellis-meta/references/customize-local/change-workflow.md": "43fa780a2ca580de121b10893d49b99f978873deebbf45008c466e5ac6651519", + ".pi/skills/trellis-meta/references/customize-local/overview.md": "ce8f09e9f93ce9a48500763fb3a4db2b3908a5fbf4f985ab71dacebb404cf8f4", + ".pi/skills/trellis-meta/references/local-architecture/bundled-skills.md": "bac739b042d7a12dae5d14d85cbc312e56ca03f571d2ba7a25b20641ddaefc3f", + ".pi/skills/trellis-meta/references/local-architecture/context-injection.md": "8497289bf333b3aa456f317039d1239b7ece79254aa0eb62cfc647714c866084", + ".pi/skills/trellis-meta/references/local-architecture/generated-files.md": "1128d45b2e8c011c247bcf3a3ca1184ca4951fb34a0802c50bf3fecbd7e4de55", + ".pi/skills/trellis-meta/references/local-architecture/multi-agent-channel.md": "56e5070474aeca872e2d70c46feea5aaafecd3d3ec052c3f7b1877358dca62e9", + ".pi/skills/trellis-meta/references/local-architecture/overview.md": "50638fd9eaaba2e0edf2f2a84d920578d5b2fb7934031b174e417d3dc510b2a6", + ".pi/skills/trellis-meta/references/local-architecture/spec-system.md": "b8d8a6a0888b44a232c8f50161b9e20e903cf621ad7be4021715ab6fab226f47", + ".pi/skills/trellis-meta/references/local-architecture/task-system.md": "2b561d49c390f7d0db5391912946133be4bf73189231e2b8cc9afa1c5ac6165a", + ".pi/skills/trellis-meta/references/local-architecture/workflow.md": "cfcdc6e4468a5d9c816e929fcca01640cd41cfdaaa4824118b40a8e460c927b6", + ".pi/skills/trellis-meta/references/local-architecture/workspace-memory.md": "e6427b46aba744563c2444b30df4043cd856561b7709ec2dece26095416421fd", + ".pi/skills/trellis-meta/references/platform-files/agents.md": "ce1a22c2b3ff0bff68755b5663e1f3b63ef2f1658cff43e511cee9013537c698", + ".pi/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "acdeba5866ccbe0e68fffc847e77e726cf79f579898e61312f6a8d62b48380ff", + ".pi/skills/trellis-meta/references/platform-files/overview.md": "821a2718b38e17439c2bb487a6dd3a95c8138e83dbda60b142a00ff80178dc3c", + ".pi/skills/trellis-meta/references/platform-files/platform-map.md": "59e13cdaa2077149b5d82ad25fa5dd821195be30bdc43d853619c4702d334a6e", + ".pi/skills/trellis-meta/references/platform-files/skills-and-commands.md": "5732bb0a7d384a91abd0c103372f3865e0b186e75185f35e0e91b00a6b6bf5a3", + ".pi/skills/trellis-meta/SKILL.md": "a190ecbde70a658a5b5c3e2558783e99c9afc6f2e5cdecdac8f11c06dd42065e", + ".pi/skills/trellis-session-insight/references/cli-quick-reference.md": "c520353fe3fc00b9702ed4f780647c8fbd6d342e66527a03647f533a9fe09779", + ".pi/skills/trellis-session-insight/references/triggering-patterns.md": "121ecd23be83d1567e8ce15c366a81073d7a2b1d3ad616fce235c07ca1f1cc20", + ".pi/skills/trellis-session-insight/SKILL.md": "f6ad80eaca21b8fa63d31ae26b612bb2bb69dd04469603828509a8c5a0463ff1", + ".pi/skills/trellis-spec-bootstrap/references/mcp-setup.md": "df542fc8f279edd38046d26a7c8151804b708f57b24d4aa2733cea587a88c65e", + ".pi/skills/trellis-spec-bootstrap/references/repository-analysis.md": "0dae98d774f6e34559b9f3442888ac43e3a8af110c37cbefc49ce256986858b6", + ".pi/skills/trellis-spec-bootstrap/references/spec-task-planning.md": "ef493d028c3b0807a8a534bb71fb92a68129f273db763ad27ceb464a522e799d", + ".pi/skills/trellis-spec-bootstrap/references/spec-writing.md": "e9800fe9ed4a4cd87062ea1829cf2caa8d170ec15e141678a6a30e74c497f47d", + ".pi/skills/trellis-spec-bootstrap/SKILL.md": "97bfa68c06cebb558eb4464bc1b81f7d2d56040d75baa8de1ee5ad90cca0196a", + ".pi/agents/trellis-check.md": "1dbfedd3403f201fbfdbae8d810afba0a1f812b97f0f8e308908db7eaceea496", + ".pi/agents/trellis-implement.md": "9bb1f70d09b7104a671ef9a0ba072b4500d45b8556a253126a003e2b1e7281a2", + ".pi/agents/trellis-research.md": "ef77555f4c2c4ade36f1c23a076b6f4bb9d180ff24a2f00d7cdc2f8fd5af0b0a", + ".pi/extensions/trellis/index.ts": "1b82e383661077daa09a5e26193499a1f01c547834b44ebcd1fe496b730b6d75", + ".pi/settings.json": "66cc59c9b410b267cd081c5a312aea3f32d82ed30c254cef6b4d99248d3bea50", + ".trae/commands/trellis-continue.md": "c1cd356883657e75b8e36c282853f94279cbd6cf393a9a953acc04713814cab8", + ".trae/commands/trellis-finish-work.md": "93ffc50e3c2af48d082075a761e3028498913c405f8fc043ac12d9726990c92a", + ".trae/skills/trellis-before-dev/SKILL.md": "859894f2e8258cbfa142d710363433761c54984c69f7ed7bd44512fb4eb165cd", + ".trae/skills/trellis-brainstorm/SKILL.md": "3bbfb6506af3c318c6f9b3c280517ce93a02e40e27535de7ff5fc029d3027860", + ".trae/skills/trellis-break-loop/SKILL.md": "f5a93699832f29dee443b53c135a7459519b371f689af191d15c29e8ee5c7bde", + ".trae/skills/trellis-check/SKILL.md": "b21ff04b7680ebacb8c5ecbc48a22d627eb13e2b47fceb78c8ced0b43b60b282", + ".trae/skills/trellis-update-spec/SKILL.md": "cef32aec88db973a0a0272cf18b91d4585fe6ed6625e3de06851a5d3402f65d6", + ".trae/skills/trellis-channel/references/command-reference.md": "c4df3d940d89814310bfdaded20e84dd1d6b96786f6772688de7a123338d3c8e", + ".trae/skills/trellis-channel/references/forum.md": "407db39e8ebde47cea8637e3069bdb3d76ba42b43ed1d08ba97a56f26dbcfcb4", + ".trae/skills/trellis-channel/references/progress-debugging.md": "93b021fd8d5d3d4ee54952c40424ab91ca57c2ab8032bfbfc3b5c5e22f1464cd", + ".trae/skills/trellis-channel/references/workers.md": "d4481b2858bca82050cb8d131916da8a2ccc3857f199cdb9ced8bb41d592e2a7", + ".trae/skills/trellis-channel/references/workflows.md": "0782d602efde6e94a7d7d7950fd605eedc9a1f5a523908284e76b628fd618fd4", + ".trae/skills/trellis-channel/SKILL.md": "ff84801c511a736da018b4b1149348ebc9a16b7fc18d905ec10cfa7acbf269a3", + ".trae/skills/trellis-meta/references/customize-local/add-project-local-conventions.md": "86009ccb5d0373f399582da0bc570c4e5c6053c3c764857424ff93384f0e04e5", + ".trae/skills/trellis-meta/references/customize-local/change-agents.md": "3eef0d9b9cf875f121c1a38afb8eee6d3cd3894127db601ae5a7760fa5b61575", + ".trae/skills/trellis-meta/references/customize-local/change-context-loading.md": "350d319dc1ab99609ddbf52cf8c06c71bd97ba1a29ce2eac8b97d0bb192938bf", + ".trae/skills/trellis-meta/references/customize-local/change-hooks.md": "4c18b134f05d5ba1609c517ec94c97c9442b5f0670aa2211c487c31ed2a4f358", + ".trae/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "a6994e2418cdf5bcad10b4236b02741179ae794bc4fdd811a64f443293b69268", + ".trae/skills/trellis-meta/references/customize-local/change-spec-structure.md": "31eccaad7097d96e66a45c1b4caea1ba4f2e54b7814184c3ebf82c87dafc4841", + ".trae/skills/trellis-meta/references/customize-local/change-task-lifecycle.md": "60ff9efb93604b87a461a4af30322d76750402a51e40f31531a7ff88d309996d", + ".trae/skills/trellis-meta/references/customize-local/change-workflow.md": "43fa780a2ca580de121b10893d49b99f978873deebbf45008c466e5ac6651519", + ".trae/skills/trellis-meta/references/customize-local/overview.md": "ce8f09e9f93ce9a48500763fb3a4db2b3908a5fbf4f985ab71dacebb404cf8f4", + ".trae/skills/trellis-meta/references/local-architecture/bundled-skills.md": "bac739b042d7a12dae5d14d85cbc312e56ca03f571d2ba7a25b20641ddaefc3f", + ".trae/skills/trellis-meta/references/local-architecture/context-injection.md": "8497289bf333b3aa456f317039d1239b7ece79254aa0eb62cfc647714c866084", + ".trae/skills/trellis-meta/references/local-architecture/generated-files.md": "1128d45b2e8c011c247bcf3a3ca1184ca4951fb34a0802c50bf3fecbd7e4de55", + ".trae/skills/trellis-meta/references/local-architecture/multi-agent-channel.md": "56e5070474aeca872e2d70c46feea5aaafecd3d3ec052c3f7b1877358dca62e9", + ".trae/skills/trellis-meta/references/local-architecture/overview.md": "50638fd9eaaba2e0edf2f2a84d920578d5b2fb7934031b174e417d3dc510b2a6", + ".trae/skills/trellis-meta/references/local-architecture/spec-system.md": "b8d8a6a0888b44a232c8f50161b9e20e903cf621ad7be4021715ab6fab226f47", + ".trae/skills/trellis-meta/references/local-architecture/task-system.md": "2b561d49c390f7d0db5391912946133be4bf73189231e2b8cc9afa1c5ac6165a", + ".trae/skills/trellis-meta/references/local-architecture/workflow.md": "cfcdc6e4468a5d9c816e929fcca01640cd41cfdaaa4824118b40a8e460c927b6", + ".trae/skills/trellis-meta/references/local-architecture/workspace-memory.md": "e6427b46aba744563c2444b30df4043cd856561b7709ec2dece26095416421fd", + ".trae/skills/trellis-meta/references/platform-files/agents.md": "ce1a22c2b3ff0bff68755b5663e1f3b63ef2f1658cff43e511cee9013537c698", + ".trae/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "acdeba5866ccbe0e68fffc847e77e726cf79f579898e61312f6a8d62b48380ff", + ".trae/skills/trellis-meta/references/platform-files/overview.md": "821a2718b38e17439c2bb487a6dd3a95c8138e83dbda60b142a00ff80178dc3c", + ".trae/skills/trellis-meta/references/platform-files/platform-map.md": "59e13cdaa2077149b5d82ad25fa5dd821195be30bdc43d853619c4702d334a6e", + ".trae/skills/trellis-meta/references/platform-files/skills-and-commands.md": "5732bb0a7d384a91abd0c103372f3865e0b186e75185f35e0e91b00a6b6bf5a3", + ".trae/skills/trellis-meta/SKILL.md": "a190ecbde70a658a5b5c3e2558783e99c9afc6f2e5cdecdac8f11c06dd42065e", + ".trae/skills/trellis-session-insight/references/cli-quick-reference.md": "c520353fe3fc00b9702ed4f780647c8fbd6d342e66527a03647f533a9fe09779", + ".trae/skills/trellis-session-insight/references/triggering-patterns.md": "121ecd23be83d1567e8ce15c366a81073d7a2b1d3ad616fce235c07ca1f1cc20", + ".trae/skills/trellis-session-insight/SKILL.md": "f6ad80eaca21b8fa63d31ae26b612bb2bb69dd04469603828509a8c5a0463ff1", + ".trae/skills/trellis-spec-bootstrap/references/mcp-setup.md": "df542fc8f279edd38046d26a7c8151804b708f57b24d4aa2733cea587a88c65e", + ".trae/skills/trellis-spec-bootstrap/references/repository-analysis.md": "0dae98d774f6e34559b9f3442888ac43e3a8af110c37cbefc49ce256986858b6", + ".trae/skills/trellis-spec-bootstrap/references/spec-task-planning.md": "ef493d028c3b0807a8a534bb71fb92a68129f273db763ad27ceb464a522e799d", + ".trae/skills/trellis-spec-bootstrap/references/spec-writing.md": "e9800fe9ed4a4cd87062ea1829cf2caa8d170ec15e141678a6a30e74c497f47d", + ".trae/skills/trellis-spec-bootstrap/SKILL.md": "97bfa68c06cebb558eb4464bc1b81f7d2d56040d75baa8de1ee5ad90cca0196a", + ".trae/agents/trellis-check.md": "86ed37a6cdb5ae39fe20692d84a0529f4cf74fc83a69fb83ee0e1a24ea5a94f1", + ".trae/agents/trellis-implement.md": "96b78811300bc70309eef390995e9e1f222e5eaa4ee55c476ed3496e261da0f2", + ".trae/agents/trellis-research.md": "7ebd1103ba77e484823ea203d65f74963fadc14979f87aec3a48e0466571482f", + ".trae/hooks/inject-workflow-state.py": "552d4ee4ca059337856c587fde1aaf79364444d0465689faa88c43797969b7de", + ".trae/hooks/session-start.py": "688e9d5c2273575b2c1cc8db17fa39da7b1798542a67f65286d97cdbdc347995", + ".trae/hooks.json": "5904359afd81064dde5c58245871014a9ebf40d6c73cf43fc08d0060a6c5e797", + "AGENTS.md": "6cacfe99748b435d0660c2463c697bc323d53798aecf3492283ca8eac1b29682", + ".trellis/agents/check.md": "edb4f57361407249a53bf5998ebf91c40d2b969e826a2c5e1b4e813a08bcb175", + ".trellis/agents/implement.md": "66e25ad046c94869442834bc3cdfbd5a9a7412d3ff54561d64d2886552c27e87", + ".trellis/config.yaml": "3e295bf4310763240647f40b3aeee7a7c6d134142cdc826e02d850ca2407fc43", + ".trellis/scripts/__init__.py": "1242be5b972094c2e141aecbe81a4efd478f6534e3d5e28306374e6a18fcf46c", + ".trellis/scripts/add_session.py": "7fcacb29ffd13599949a517aa967d868040eb4c9b40356b0c460a32361237eb6", + ".trellis/scripts/common/__init__.py": "3d5e9347141f0296319a5beb29d69ae714c5a474b9078caeb3edd7c5f6562e22", + ".trellis/scripts/common/active_task.py": "8ac10263a88262aeaec2c600b33f3761f99d80c305718609a3bef97a2ff08938", + ".trellis/scripts/common/cli_adapter.py": "e7586505462c5449654a8fad70a50360a37a8aefa1efedfa06195bf80e9d1dae", + ".trellis/scripts/common/config.py": "25c5a53ad20d6909be5209222e4208a84528805316a4d78350529459a364edb1", + ".trellis/scripts/common/developer.py": "f5f833123abe68890171b4da825a324216d24913f6b5ad9245afc556424ffd7b", + ".trellis/scripts/common/git.py": "e14817be7de122d3a106f509c2825aeb9669d962ba73ba241642d2931cfdf1d6", + ".trellis/scripts/common/git_context.py": "fa30ced454f1a91ffc9f8b2abeb32225e3447cbdc90bad783797374eba07265d", + ".trellis/scripts/common/io.py": "6480b181f2bc505323b28ed7a66963d7b7edc96251e83b4c8e7a45907cc721c8", + ".trellis/scripts/common/log.py": "471df6895cfac80f995edebbf9974f6b7440634b7a688f28b8331c868bc0f3cf", + ".trellis/scripts/common/packages_context.py": "efe158d7c99c2268851d0216fbb08de22836e418a8dbeb73575b8cc249eed7b7", + ".trellis/scripts/common/paths.py": "05898ef136cc7c4d861b05fbf2b16d53ddd3e6f311a231d4fcfcb81bde7c45ee", + ".trellis/scripts/common/safe_commit.py": "baa5c82324eb62154374ec63394ecdc8609bb37d93892e3bcb88f452bb7d6446", + ".trellis/scripts/common/session_context.py": "df79c44efe3432811c32d145d57a66343a70e221ec087ed2bd28b76677bb4076", + ".trellis/scripts/common/task_context.py": "d174684d417bbe2fafc26b6afcddb264c7dc519527bb24d2055cd27daaad9b55", + ".trellis/scripts/common/task_queue.py": "0be61f713462b1fe4574927c82fc4704e678afe72dcb9813543aedf2f9e9e0c5", + ".trellis/scripts/common/task_store.py": "1019cb5e262001d01960ae0d9751a9a5346aafccb1695d4be474dbc18be5de6c", + ".trellis/scripts/common/task_utils.py": "f5ef4af87ba3e11d8b19630c0c96d009de1811fc9be56c2027a9c96e21ed103e", + ".trellis/scripts/common/tasks.py": "4436a8b0b53c270a35989e26d9dbd92669408c6562d88c02083a404562da85fe", + ".trellis/scripts/common/trellis_config.py": "0839dcf90ebbd77712c276930a89335b3313927051650c91d220fb51ca2a6a3c", + ".trellis/scripts/common/types.py": "9962081cc2608fb9d1deb32c6880e336f62cdca6b338e7ae813304701e155ee9", + ".trellis/scripts/common/workflow_phase.py": "f2b5fcf0c40cedcf3d7d0ad8023d141fa4095caf0302d73f2a73e1bc04b5692b", + ".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": "6c65801a1f56648fd4765a1d216493d3094827c1db4761e55fdaa548c1801798", + ".trellis/workflow.md": "078bc526d7a29b1d391cc198d113d28225cf46a6868d655498832a6cc9a36acf", + ".trellis/spec/README.md": "40dfb9fdacc5c24e85a65fad92984af638339ee12dccabffa8b1f1ce68f9096b", + ".trellis/spec/backend/ai-sdk-integration.md": "4effa6c48cfa03286bc334014c195c981b52d7b097f910c8e14ae441f4edbe16", + ".trellis/spec/backend/authentication.md": "123994afc32105ccab2d3b3ab71de5363a93035749faa0c0f029f08b958927b6", + ".trellis/spec/backend/database.md": "b9efab775a1a7f4cd55d909af667107dfee13e03963a8b8b1183e6a4bbebfd70", + ".trellis/spec/backend/directory-structure.md": "25b2355111c7b15096bc8e7eb5d0d1470a1dc32ce5c4e09369d362c8940fe35c", + ".trellis/spec/backend/index.md": "8af39732405c088fc8da4f376b19e9ee6cf8242ee24e7c48a5a96e2261506ca4", + ".trellis/spec/backend/logging.md": "30c6f062fa4d18bb7cd84a3483a49e114d83831a031b95faa77deb3bdeed6d8c", + ".trellis/spec/backend/orpc-usage.md": "734d417ce460f5aae1813da13ac5fa9a2cf348fc9f8d1de3a4735fb5cb2820d3", + ".trellis/spec/backend/performance.md": "42060b1ee2addedabad6af5e3938e371558e0ecb1f4e6fc7c97b6e3294a83d65", + ".trellis/spec/backend/quality.md": "f9f2295923faf514ded044fec613f4a97d9fc25ecdc0695320d2a2892e46f5d6", + ".trellis/spec/backend/type-safety.md": "070b40889098eaed88332cbcdd54aa211300a8e6457a7f05f63c7bd1a0c22929", + ".trellis/spec/big-question/index.md": "d4887ee011998d5820f11c59ba1b17ce7a31b41f61564183724602208b4b5c79", + ".trellis/spec/big-question/postgres-json-jsonb.md": "6dc6597756ab2ccf5ef15d50b4b76a78fb10b2cc2222c6380ac8cf435756135b", + ".trellis/spec/big-question/sentry-nextintl-conflict.md": "bb5dd8f4db9646ad27b910c8abc922bf2380820dd2a6a0cf3dbb0c46d7c9c7c8", + ".trellis/spec/big-question/turbopack-webpack-flexbox.md": "11b606360b37d9726efaa3fd24175dc1cccff1059c2dabc4e92296a6a7eb692a", + ".trellis/spec/big-question/webkit-tap-highlight.md": "d2340dfe7ba49f21d5587bdd7e724c28d1e6b42fd48c48bd8c4a088f73803805", + ".trellis/spec/frontend/ai-sdk-integration.md": "3c93feb6bb567354a17e6de97f417182bcd3cfb6e52bbe17002f791e4650ea56", + ".trellis/spec/frontend/api-integration.md": "dced0eea574a7deba428fbfba050a3ac89f38fffbdba095c3a98b8898fc1a3a0", + ".trellis/spec/frontend/authentication.md": "b4dc99456c937122a9c6998c1b57083d408fd774ecf6a0d5ecf2be9fb4a8618f", + ".trellis/spec/frontend/components.md": "157eac19346073a5d41fec288e4d5417f6e8e9cdd32568f347779dcdcae011b3", + ".trellis/spec/frontend/css-layout.md": "932513eae947d921b4e6a42dbcb0c53465ac11a9b1780acae325241ee631d1ac", + ".trellis/spec/frontend/directory-structure.md": "3a3206a5bb673553f3e428b4bb5717da9a2b978a3e9b2d757d234f904f53ac95", + ".trellis/spec/frontend/hooks.md": "96c3263997d8c40b5065fc4dc609a079864ef34da522cb9f52fe0b3dff48ab7d", + ".trellis/spec/frontend/index.md": "3d9beb1b41664da116d2d921126bf3f90e5cb0880eb6d30427bc89c2780d9b93", + ".trellis/spec/frontend/orpc-usage.md": "7724f962587bb6f055ce15d73c6c5893e80178d26465b7fb49174edf291d88d2", + ".trellis/spec/frontend/quality.md": "2e6f18347350ac5b3417542329647dfae1e5c81831b1f622a4c481efa9e93666", + ".trellis/spec/frontend/state-management.md": "50032924868267cbd8de2bf4ac49c17cab42a89d5471ce27b49486b32d19112a", + ".trellis/spec/frontend/type-safety.md": "ff8cfaaaa4b97654d8bb72a10e9d0f58139cef595db1e7040d6e3f874f06ae97", + ".trellis/spec/guides/cross-layer-thinking-guide.md": "ef671e8e1a35362af065077051f6812d871a48de1b3dd801020572ae9ba54410", + ".trellis/spec/guides/index.md": "e73055bc4b37b91bd57ba57c8bfd92e730ec5755a860c81fa56718af9ae011b7", + ".trellis/spec/guides/pre-implementation-checklist.md": "cbf6f379755849d09846716ddec7146702888ccc094a940774abe32d98334271", + ".trellis/spec/shared/code-quality.md": "b2dff1fef8f0f94d168ea0b6a7d31d54c28a33639811bf5589eb3ec7d3b4bbba", + ".trellis/spec/shared/dependencies.md": "1dcfc9467870f006648cb133129b94345f87fc515d8fc532cb9820c0c10e37f6", + ".trellis/spec/shared/index.md": "ae4f000c5e35d7ca003ca7b736ccbf46f6a666a67db5c8937e0e62aeba6a6f44", + ".trellis/spec/shared/typescript.md": "6840bc24db8dcedfb30a03f490e3fc128fc915f1165396876d09c2bd441bb458" + } +} \ No newline at end of file diff --git a/.trellis/.version b/.trellis/.version new file mode 100644 index 0000000..e0ea44c --- /dev/null +++ b/.trellis/.version @@ -0,0 +1 @@ +0.6.5 \ No newline at end of file diff --git a/.trellis/agents/check.md b/.trellis/agents/check.md new file mode 100644 index 0000000..6c1bf13 --- /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 0000000..3262f79 --- /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 new file mode 100644 index 0000000..002a712 --- /dev/null +++ b/.trellis/config.yaml @@ -0,0 +1,110 @@ +# Trellis Configuration +# Project-level settings for the Trellis workflow system +# +# All values have sensible defaults. Only override what you need. + +#------------------------------------------------------------------------------- +# Session Recording +#------------------------------------------------------------------------------- + +# Commit message used when auto-committing journal/index changes +# after running add_session.py +session_commit_message: "chore: record journal" + +# Maximum lines per journal file before rotating to a new one +max_journal_lines: 2000 + +#------------------------------------------------------------------------------- +# Session Auto-Commit +#------------------------------------------------------------------------------- + +# Auto-commit behavior for session journal + task archive operations. +# - true (default): scripts auto-stage and auto-commit journal / task changes +# after add_session.py / task.py archive runs. +# - false: scripts do not touch git. Files (journal-*.md, task archive moves) +# are still written to disk; you decide whether to git add / commit. +# +# Use `false` if your project's .gitignore intentionally excludes `.trellis/` +# and you want session data kept local-only, or if you prefer to review +# staged changes manually before each commit. +# +# Accepts: true / false / yes / no / 1 / 0 / on / off (case-insensitive). +# +# session_auto_commit: true + +#------------------------------------------------------------------------------- +# Task Lifecycle Hooks +#------------------------------------------------------------------------------- + +# Shell commands to run after task lifecycle events. +# Each hook receives TASK_JSON_PATH environment variable pointing to task.json. +# Hook failures print a warning but do not block the main operation. +# +# hooks: +# after_create: +# - "echo 'Task created'" +# after_start: +# - "echo 'Task started'" +# after_finish: +# - "echo 'Task finished'" +# after_archive: +# - "echo 'Task archived'" + +#------------------------------------------------------------------------------- +# Monorepo / Packages +#------------------------------------------------------------------------------- + +# Declare packages for monorepo projects. +# Trellis auto-detects workspaces during `trellis init`, but you can also +# configure them manually here. +# +# packages: +# frontend: +# path: packages/frontend +# backend: +# path: packages/backend +# docs: +# path: docs-site +# type: submodule +# # For polyrepo / meta-repo layouts (independent .git in each subdir), +# # mark the package with `git: true`. The runtime treats it as an +# # independent repository for things like git-context display. +# webapp: +# path: ./webapp +# git: true + +# Default package used when --package is not specified. +# default_package: frontend + +#------------------------------------------------------------------------------- +# 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. +# +channel: + worker_guard: + idle_timeout: 5m + max_live_workers: 6 + +#------------------------------------------------------------------------------- +# 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: +# dispatch_mode: inline # or "sub-agent" to dispatch trellis-* sub-agents diff --git a/.trellis/scripts/__init__.py b/.trellis/scripts/__init__.py new file mode 100755 index 0000000..815a137 --- /dev/null +++ b/.trellis/scripts/__init__.py @@ -0,0 +1,5 @@ +""" +Trellis Python Scripts + +This module provides Python implementations of Trellis workflow scripts. +""" diff --git a/.trellis/scripts/add_session.py b/.trellis/scripts/add_session.py new file mode 100755 index 0000000..6d25829 --- /dev/null +++ b/.trellis/scripts/add_session.py @@ -0,0 +1,567 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Add a new session to journal file and update index.md. + +Usage: + python3 add_session.py --title "Title" --commit "hash" --summary "Summary" [--package cli] + python3 add_session.py --title "Title" --branch "feat/my-branch" + + # Pipe detailed content via stdin (use --stdin to opt in): + cat << 'EOF' | python3 add_session.py --stdin --title "Title" --summary "Summary" + <session content here> + EOF + +Branch resolution order: + 1. --branch CLI arg (explicit) + 2. task.json branch field (from active task) + 3. git branch --show-current (auto-detect) + 4. None (omitted gracefully) +""" + +from __future__ import annotations + +import argparse +import re +import sys +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, + get_developer, + get_workspace_dir, +) +from common.developer import ensure_developer +from common.git import run_git +from common.safe_commit import ( + print_gitignore_warning, + safe_git_add, + safe_trellis_paths_to_add, +) +from common.tasks import load_task +from common.config import ( + get_packages, + get_session_auto_commit, + get_session_commit_message, + get_max_journal_lines, + is_monorepo, + resolve_package, + validate_package, +) + + +# ============================================================================= +# Helper Functions +# ============================================================================= + +def get_latest_journal_info(dev_dir: Path) -> tuple[Path | None, int, int]: + """Get latest journal file info. + + Returns: + Tuple of (file_path, file_number, line_count). + """ + latest_file: Path | None = None + latest_num = -1 + + for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"): + if not f.is_file(): + continue + + match = re.search(r"(\d+)$", f.stem) + if match: + num = int(match.group(1)) + if num > latest_num: + latest_num = num + latest_file = f + + if latest_file: + lines = len(latest_file.read_text(encoding="utf-8").splitlines()) + return latest_file, latest_num, lines + + return None, 0, 0 + + +def get_current_session(index_file: Path) -> int: + """Get current session number from index.md.""" + if not index_file.is_file(): + return 0 + + content = index_file.read_text(encoding="utf-8") + for line in content.splitlines(): + if "Total Sessions" in line: + match = re.search(r":\s*(\d+)", line) + if match: + return int(match.group(1)) + return 0 + + +def _extract_journal_num(filename: str) -> int: + """Extract journal number from filename for sorting.""" + match = re.search(r"(\d+)", filename) + return int(match.group(1)) if match else 0 + + +def count_journal_files(dev_dir: Path, active_num: int) -> str: + """Count journal files and return table rows.""" + active_file = f"{FILE_JOURNAL_PREFIX}{active_num}.md" + result_lines = [] + + files = sorted( + [f for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md") if f.is_file()], + key=lambda f: _extract_journal_num(f.stem), + reverse=True + ) + + for f in files: + filename = f.name + lines = len(f.read_text(encoding="utf-8").splitlines()) + status = "Active" if filename == active_file else "Archived" + result_lines.append(f"| `{filename}` | ~{lines} | {status} |") + + return "\n".join(result_lines) + + +def create_new_journal_file( + dev_dir: Path, num: int, developer: str, today: str, max_lines: int = 2000, +) -> Path: + """Create a new journal file.""" + prev_num = num - 1 + new_file = dev_dir / f"{FILE_JOURNAL_PREFIX}{num}.md" + + content = f"""# Journal - {developer} (Part {num}) + +> Continuation from `{FILE_JOURNAL_PREFIX}{prev_num}.md` (archived at ~{max_lines} lines) +> Started: {today} + +--- + +""" + new_file.write_text(content, encoding="utf-8") + return new_file + + +def generate_session_content( + session_num: int, + title: str, + commit: str, + summary: str, + extra_content: str, + today: str, + package: str | None = None, + branch: str | None = None, +) -> str: + """Generate session content.""" + if commit and commit != "-": + commit_table = """| Hash | Message | +|------|---------|""" + for c in commit.split(","): + c = c.strip() + commit_table += f"\n| `{c}` | (see git log) |" + else: + commit_table = "(No commits - planning session)" + + package_line = f"\n**Package**: {package}" if package else "" + branch_line = f"\n**Branch**: `{branch}`" if branch else "" + + return f""" + +## Session {session_num}: {title} + +**Date**: {today} +**Task**: {title}{package_line}{branch_line} + +### Summary + +{summary} + +### Main Changes + +{extra_content} + +### Git Commits + +{commit_table} + +### Testing + +- [OK] (Add test results) + +### Status + +[OK] **Completed** + +### Next Steps + +- None - task complete +""" + + +def update_index( + index_file: Path, + dev_dir: Path, + title: str, + commit: str, + new_session: int, + active_file: str, + today: str, + branch: str | None = None, +) -> bool: + """Update index.md with new session info.""" + # Format commit for display + commit_display = "-" + if commit and commit != "-": + commit_display = re.sub(r"([a-f0-9]{7,})", r"`\1`", commit.replace(",", ", ")) + + # Get file number from active_file name + match = re.search(r"(\d+)", active_file) + active_num = int(match.group(1)) if match else 0 + files_table = count_journal_files(dev_dir, active_num) + + print(f"Updating index.md for session {new_session}...") + print(f" Title: {title}") + print(f" Commit: {commit_display}") + print(f" Active File: {active_file}") + print() + + content = index_file.read_text(encoding="utf-8") + + if "@@@auto:current-status" not in content: + print("Error: Markers not found in index.md. Please ensure markers exist.", file=sys.stderr) + return False + + # Process sections + lines = content.splitlines() + new_lines = [] + + in_current_status = False + in_active_documents = False + in_session_history = False + header_written = False + + for line in lines: + if "@@@auto:current-status" in line: + new_lines.append(line) + in_current_status = True + new_lines.append(f"- **Active File**: `{active_file}`") + new_lines.append(f"- **Total Sessions**: {new_session}") + new_lines.append(f"- **Last Active**: {today}") + continue + + if "@@@/auto:current-status" in line: + in_current_status = False + new_lines.append(line) + continue + + if "@@@auto:active-documents" in line: + new_lines.append(line) + in_active_documents = True + new_lines.append("| File | Lines | Status |") + new_lines.append("|------|-------|--------|") + new_lines.append(files_table) + continue + + if "@@@/auto:active-documents" in line: + in_active_documents = False + new_lines.append(line) + continue + + if "@@@auto:session-history" in line: + new_lines.append(line) + in_session_history = True + header_written = False + continue + + if "@@@/auto:session-history" in line: + in_session_history = False + new_lines.append(line) + continue + + if in_current_status: + continue + + if in_active_documents: + continue + + if in_session_history: + # Migrate old 4/6-column headers to 5-column Branch-only history. + if re.match( + r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*Base Branch\s*\|\s*$", + line, + ): + new_lines.append("| # | Date | Title | Commits | Branch |") + continue + if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*$", line): + new_lines.append("| # | Date | Title | Commits | Branch |") + continue + if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*$", line): + new_lines.append("| # | Date | Title | Commits | Branch |") + continue + if re.match(r"^\|[-| ]+\|\s*$", line) and not header_written: + new_lines.append("|---|------|-------|---------|--------|") + new_lines.append(f"| {new_session} | {today} | {title} | {commit_display} | `{branch or '-'}` |") + header_written = True + continue + new_lines.append(line) + continue + + new_lines.append(line) + + index_file.write_text("\n".join(new_lines), encoding="utf-8") + print("[OK] Updated index.md successfully!") + return True + + +# ============================================================================= +# Main Function +# ============================================================================= + +def _auto_commit_workspace(repo_root: Path) -> None: + """Stage Trellis-owned workspace + current-task paths and commit. + + 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 + (journal/index files are still written to disk by the caller). + """ + if not get_session_auto_commit(repo_root): + print( + "[OK] session_auto_commit: false — skipping git stage/commit.", + file=sys.stderr, + ) + return + + commit_msg = get_session_commit_message(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 + + success, _, err = safe_git_add(paths, repo_root) + if not success: + if err and "ignored by" in err.lower(): + print_gitignore_warning(paths) + else: + print( + f"[WARN] git add failed: {err.strip() if err else 'unknown error'}", + file=sys.stderr, + ) + return + + # Check if there are staged changes for the paths we just staged. + rc, _, _ = run_git( + ["diff", "--cached", "--quiet", "--", *paths], cwd=repo_root + ) + if rc == 0: + print("[OK] No workspace changes to commit.", file=sys.stderr) + return + + rc, _, commit_err = run_git(["commit", "-m", commit_msg], cwd=repo_root) + if rc == 0: + print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr) + else: + print( + f"[WARN] Auto-commit failed: {commit_err.strip()}", + file=sys.stderr, + ) + + +def add_session( + title: str, + commit: str = "-", + summary: str = "(Add summary)", + extra_content: str = "(Add details)", + auto_commit: bool = True, + package: str | None = None, + branch: str | None = None, +) -> int: + """Add a new session.""" + repo_root = get_repo_root() + ensure_developer(repo_root) + + developer = get_developer(repo_root) + if not developer: + print("Error: Developer not initialized", file=sys.stderr) + return 1 + + dev_dir = get_workspace_dir(repo_root) + if not dev_dir: + print("Error: Workspace directory not found", file=sys.stderr) + return 1 + + max_lines = get_max_journal_lines(repo_root) + + index_file = dev_dir / "index.md" + today = datetime.now().strftime("%Y-%m-%d") + + journal_file, current_num, current_lines = get_latest_journal_info(dev_dir) + current_session = get_current_session(index_file) + new_session = current_session + 1 + + session_content = generate_session_content( + new_session, title, commit, summary, extra_content, today, package, + branch, + ) + content_lines = len(session_content.splitlines()) + + print("========================================", file=sys.stderr) + print("ADD SESSION", file=sys.stderr) + print("========================================", file=sys.stderr) + print("", file=sys.stderr) + print(f"Session: {new_session}", file=sys.stderr) + print(f"Title: {title}", file=sys.stderr) + print(f"Commit: {commit}", file=sys.stderr) + print("", file=sys.stderr) + print(f"Current journal file: {FILE_JOURNAL_PREFIX}{current_num}.md", file=sys.stderr) + print(f"Current lines: {current_lines}", file=sys.stderr) + print(f"New content lines: {content_lines}", file=sys.stderr) + print(f"Total after append: {current_lines + content_lines}", file=sys.stderr) + print("", file=sys.stderr) + + target_file = journal_file + target_num = current_num + + if current_lines + content_lines > max_lines: + target_num = current_num + 1 + print(f"[!] Exceeds {max_lines} lines, creating {FILE_JOURNAL_PREFIX}{target_num}.md", file=sys.stderr) + target_file = create_new_journal_file(dev_dir, target_num, developer, today, max_lines) + print(f"Created: {target_file}", file=sys.stderr) + + # Append session content + if target_file: + with target_file.open("a", encoding="utf-8") as f: + f.write(session_content) + print(f"[OK] Appended session to {target_file.name}", file=sys.stderr) + + print("", file=sys.stderr) + + # Update index.md + active_file = f"{FILE_JOURNAL_PREFIX}{target_num}.md" + if not update_index( + index_file, + dev_dir, + title, + commit, + new_session, + active_file, + today, + branch, + ): + return 1 + + print("", file=sys.stderr) + print("========================================", file=sys.stderr) + print(f"[OK] Session {new_session} added successfully!", file=sys.stderr) + print("========================================", file=sys.stderr) + print("", file=sys.stderr) + print("Files updated:", file=sys.stderr) + print(f" - {target_file.name if target_file else 'journal'}", file=sys.stderr) + print(" - index.md", file=sys.stderr) + + # Auto-commit workspace changes + if auto_commit: + print("", file=sys.stderr) + _auto_commit_workspace(repo_root) + + return 0 + + +# ============================================================================= +# Main Entry +# ============================================================================= + +def main() -> int: + """CLI entry point.""" + parser = argparse.ArgumentParser( + description="Add a new session to journal file and update index.md" + ) + parser.add_argument("--title", required=True, help="Session title") + parser.add_argument("--commit", default="-", help="Comma-separated commit hashes") + parser.add_argument("--summary", default="(Add summary)", help="Brief summary") + parser.add_argument("--content-file", help="Path to file with detailed content") + parser.add_argument("--package", help="Package name tag (e.g., cli, docs-site)") + parser.add_argument("--branch", help="Branch name (auto-detected if omitted)") + parser.add_argument("--no-commit", action="store_true", + help="Skip auto-commit of workspace changes") + parser.add_argument("--stdin", action="store_true", + help="Read extra content from stdin (explicit opt-in)") + + args = parser.parse_args() + + extra_content = "(Add details)" + if args.content_file: + content_path = Path(args.content_file) + if content_path.is_file(): + extra_content = content_path.read_text(encoding="utf-8") + elif args.stdin: + extra_content = sys.stdin.read() + + # Load active task once — shared by package and branch resolution + repo_root = get_repo_root() + current = get_current_task(repo_root) + task_data = load_task(repo_root / current) if current else None + + package = args.package + if package: + # CLI source: fail-fast in monorepo, ignore in single-repo + if not is_monorepo(repo_root): + print("Warning: --package ignored in single-repo project", file=sys.stderr) + package = None + elif not validate_package(package, repo_root): + packages = get_packages(repo_root) + available = ", ".join(sorted(packages.keys())) if packages else "(none)" + print(f"Error: unknown package '{package}'. Available: {available}", file=sys.stderr) + return 1 + else: + # Inferred: active task's task.json.package → default_package → None + task_package = task_data.package if task_data else None + package = resolve_package(task_package, repo_root) + + # Resolve branch: CLI → task.json → git auto-detect → None + branch = args.branch + + if not branch: + if task_data and task_data.raw.get("branch"): + branch = task_data.raw["branch"] + else: + _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root) + detected = branch_out.strip() + if detected: + branch = detected + + return add_session( + args.title, args.commit, args.summary, extra_content, + auto_commit=not args.no_commit, + package=package, + branch=branch, + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.trellis/scripts/common/__init__.py b/.trellis/scripts/common/__init__.py new file mode 100755 index 0000000..6d72360 --- /dev/null +++ b/.trellis/scripts/common/__init__.py @@ -0,0 +1,92 @@ +""" +Common utilities for Trellis workflow scripts. + +This module provides shared functionality used by other Trellis scripts. +""" + +import io +import sys + +# ============================================================================= +# Windows Encoding Fix (MUST be at top, before any other output) +# ============================================================================= +# On Windows, stdout defaults to the system code page (often GBK/CP936). +# This causes UnicodeEncodeError when printing non-ASCII characters. +# +# Any script that imports from common will automatically get this fix. +# ============================================================================= + + +def _configure_stream(stream: object) -> object: + """Configure a stream for UTF-8 encoding on Windows.""" + # Try reconfigure() first (Python 3.7+, more reliable) + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] + return stream + # Fallback: detach and rewrap with TextIOWrapper + elif hasattr(stream, "detach"): + return io.TextIOWrapper( + stream.detach(), # type: ignore[union-attr] + encoding="utf-8", + errors="replace", + ) + return stream + + +if sys.platform == "win32": + sys.stdout = _configure_stream(sys.stdout) # type: ignore[assignment] + sys.stderr = _configure_stream(sys.stderr) # type: ignore[assignment] + sys.stdin = _configure_stream(sys.stdin) # type: ignore[assignment] + + +def configure_encoding() -> None: + """ + Configure stdout/stderr/stdin for UTF-8 encoding on Windows. + + This is automatically called when importing from common, + but can be called manually for scripts that don't import common. + + Safe to call multiple times. + """ + global sys + if sys.platform == "win32": + sys.stdout = _configure_stream(sys.stdout) # type: ignore[assignment] + sys.stderr = _configure_stream(sys.stderr) # type: ignore[assignment] + sys.stdin = _configure_stream(sys.stdin) # type: ignore[assignment] + + +from .paths import ( + DIR_WORKFLOW, + DIR_WORKSPACE, + DIR_TASKS, + DIR_ARCHIVE, + DIR_SPEC, + DIR_SCRIPTS, + FILE_DEVELOPER, + FILE_CURRENT_TASK, + FILE_TASK_JSON, + FILE_JOURNAL_PREFIX, + get_repo_root, + get_developer, + check_developer, + get_tasks_dir, + get_workspace_dir, + get_active_journal_file, + count_lines, + get_current_task, + get_current_task_abs, + normalize_task_ref, + resolve_task_ref, + set_current_task, + clear_current_task, + has_current_task, + generate_task_date_prefix, +) + +from .active_task import ( + ActiveTask, + clear_active_task, + resolve_active_task, + resolve_context_key, + set_active_task, +) diff --git a/.trellis/scripts/common/active_task.py b/.trellis/scripts/common/active_task.py new file mode 100755 index 0000000..e490ee4 --- /dev/null +++ b/.trellis/scripts/common/active_task.py @@ -0,0 +1,628 @@ +#!/usr/bin/env python3 +"""Session-scoped active task resolution. + +The user-facing concept is a single "active task". Trellis stores that pointer +per AI session/window under `.trellis/.runtime/sessions/`; without a stable +session key there is no active task. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import sys +import time +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +DIR_WORKFLOW = ".trellis" +DIR_TASKS = "tasks" +DIR_RUNTIME = ".runtime" +DIR_SESSIONS = "sessions" +DIR_CURSOR_SHELL = "cursor-shell" +CURSOR_SHELL_TICKET_TTL_SECONDS = 30 +TASK_SESSION_COMMANDS = {"start", "current", "finish"} + +_SESSION_KEYS = ("session_id", "sessionId", "sessionID") +_CONVERSATION_KEYS = ("conversation_id", "conversationId", "conversationID") +_TRANSCRIPT_KEYS = ("transcript_path", "transcriptPath", "transcript") +_NESTED_KEYS = ("input", "properties", "event", "hook_input", "hookInput") +_KNOWN_PLATFORMS = { + "claude", + "codex", + "cursor", + "opencode", + "gemini", + "droid", + "qoder", + "codebuddy", + "kiro", + "copilot", + "pi", + "trae", +} + +_ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = ( + ("claude", ("CLAUDE_SESSION_ID", "CLAUDE_CODE_SESSION_ID")), + ("codex", ("CODEX_SESSION_ID", "CODEX_THREAD_ID")), + ("cursor", ("CURSOR_SESSION_ID",)), + ("opencode", ("OPENCODE_SESSION_ID", "OPENCODE_SESSIONID", "OPENCODE_RUN_ID")), + ("gemini", ("GEMINI_SESSION_ID",)), + ("droid", ("FACTORY_SESSION_ID", "DROID_SESSION_ID")), + ("qoder", ("QODER_SESSION_ID",)), + ("codebuddy", ("CODEBUDDY_SESSION_ID",)), + ("kiro", ("KIRO_SESSION_ID",)), + ("copilot", ("COPILOT_SESSION_ID", "COPILOT_SESSIONID")), + ("pi", ("PI_SESSION_ID", "PI_SESSIONID")), + ("trae", ("TRAE_SESSION_ID",)), +) +_ENV_CONVERSATION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = ( + ("cursor", ("CURSOR_CONVERSATION_ID", "CURSOR_CONVERSATIONID")), +) +_ENV_TRANSCRIPT_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = ( + ("claude", ("CLAUDE_TRANSCRIPT_PATH",)), + ("codex", ("CODEX_TRANSCRIPT_PATH",)), + ("cursor", ("CURSOR_TRANSCRIPT_PATH",)), + ("gemini", ("GEMINI_TRANSCRIPT_PATH",)), + ("droid", ("FACTORY_TRANSCRIPT_PATH", "DROID_TRANSCRIPT_PATH")), + ("qoder", ("QODER_TRANSCRIPT_PATH",)), + ("codebuddy", ("CODEBUDDY_TRANSCRIPT_PATH",)), +) +_ENV_PLATFORM_ALIASES = { + "claude-code": "claude", + "factory": "droid", + "factory-ai": "droid", + "github-copilot": "copilot", +} + + +@dataclass(frozen=True) +class ActiveTask: + """Resolved active task state.""" + + task_path: str | None + source_type: str + context_key: str | None = None + stale: bool = False + + @property + def source(self) -> str: + """Human-readable source label.""" + if self.source_type == "session" and self.context_key: + return f"session:{self.context_key}" + if self.source_type == "session-fallback" and self.context_key: + return f"session-fallback:{self.context_key}" + return self.source_type + + +def normalize_task_ref(task_ref: str) -> str: + """Normalize a task ref for stable storage and comparison.""" + normalized = task_ref.strip() + if not normalized: + return "" + + path_obj = Path(normalized) + if path_obj.is_absolute(): + return str(path_obj) + + normalized = normalized.replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + + if normalized.startswith(f"{DIR_TASKS}/"): + return f"{DIR_WORKFLOW}/{normalized}" + + return normalized + + +def resolve_task_ref(task_ref: str, repo_root: Path) -> Path | None: + """Resolve a task ref to an absolute task directory.""" + normalized = normalize_task_ref(task_ref) + if not normalized: + return None + + path_obj = Path(normalized) + if path_obj.is_absolute(): + return path_obj + + if normalized.startswith(f"{DIR_WORKFLOW}/"): + return repo_root / path_obj + + return repo_root / DIR_WORKFLOW / DIR_TASKS / path_obj + + +def _runtime_sessions_dir(repo_root: Path) -> Path: + return repo_root / DIR_WORKFLOW / DIR_RUNTIME / DIR_SESSIONS + + +def _sanitize_key(raw: str) -> str: + safe = re.sub(r"[^A-Za-z0-9._-]+", "_", raw.strip()) + safe = safe.strip("._-") + return safe[:160] if safe else "" + + +def _hash_value(raw: str) -> str: + return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24] + + +def _as_dict(value: Any) -> dict[str, Any] | None: + return value if isinstance(value, dict) else None + + +def _string_value(value: Any) -> str | None: + if isinstance(value, str): + stripped = value.strip() + return stripped or None + return None + + +def _lookup_string(data: dict[str, Any], keys: tuple[str, ...]) -> str | None: + for key in keys: + value = _string_value(data.get(key)) + if value: + return value + + for nested_key in _NESTED_KEYS: + nested = _as_dict(data.get(nested_key)) + if not nested: + continue + value = _lookup_string(nested, keys) + if value: + return value + + return None + + +def _detect_platform(platform_input: dict[str, Any] | None, platform: str | None) -> str: + if platform: + return _sanitize_key(platform) or "session" + if platform_input: + for key in ("_trellis_platform", "trellis_platform", "platform", "source"): + value = _string_value(platform_input.get(key)) + if value: + return _sanitize_key(value) or "session" + if _string_value(platform_input.get("cursor_version")): + return "cursor" + return "session" + + +def _context_key(platform_name: str, kind: str, value: str) -> str: + if kind == "transcript": + return f"{platform_name}_transcript_{_hash_value(value)}" + safe_value = _sanitize_key(value) + if safe_value: + return f"{platform_name}_{safe_value}" + return f"{platform_name}_{_hash_value(value)}" + + +def _iter_env_keys( + env_keys: tuple[tuple[str, tuple[str, ...]], ...], + platform_name: str | None, +) -> tuple[tuple[str, tuple[str, ...]], ...]: + if not platform_name: + return env_keys + matched = tuple((name, keys) for name, keys in env_keys if name == platform_name) + return matched + + +def _env_platform_name(platform_name: str | None) -> str | None: + if not platform_name or platform_name == "session": + return None + return _ENV_PLATFORM_ALIASES.get(platform_name, platform_name) + + +def _lookup_env_context_key(platform_name: str | None) -> str | None: + """Resolve a context key from platform-provided environment variables. + + Hooks pass `TRELLIS_CONTEXT_ID` to subprocesses they launch, but an AI-run + shell command can only see session identity if the host platform exports it + in the command environment. These names are best-effort adapters; if none + are present, there is no session-scoped active task. + """ + env_platform_name = _env_platform_name(platform_name) + + for name, keys in _iter_env_keys(_ENV_SESSION_KEYS, env_platform_name): + for key in keys: + value = _string_value(os.environ.get(key)) + if value: + return _context_key(name, "session", value) + + for name, keys in _iter_env_keys(_ENV_CONVERSATION_KEYS, env_platform_name): + for key in keys: + value = _string_value(os.environ.get(key)) + if value: + return _context_key(name, "conversation", value) + + for name, keys in _iter_env_keys(_ENV_TRANSCRIPT_KEYS, env_platform_name): + for key in keys: + value = _string_value(os.environ.get(key)) + if value: + return _context_key(name, "transcript", value) + + return None + + +def _find_repo_root_from_cwd() -> Path | None: + current = Path.cwd().resolve() + while True: + if (current / DIR_WORKFLOW).is_dir(): + return current + if current == current.parent: + return None + current = current.parent + + +def _cursor_shell_ticket_dir(repo_root: Path) -> Path: + return repo_root / DIR_WORKFLOW / DIR_RUNTIME / DIR_CURSOR_SHELL + + +def _remove_file(path: Path) -> bool: + try: + path.unlink() + return True + except OSError: + return False + + +def _task_refs_match(left: str | None, right: str | None, repo_root: Path) -> bool: + if not left or not right: + return False + left_path = resolve_task_ref(left, repo_root) + right_path = resolve_task_ref(right, repo_root) + if left_path is not None and right_path is not None: + return left_path == right_path + return normalize_task_ref(left) == normalize_task_ref(right) + + +def _pending_ticket_matches_args(ticket: dict[str, Any], repo_root: Path) -> bool: + if Path(sys.argv[0]).name != "task.py": + return False + args = tuple(sys.argv[1:]) + if not args: + return False + + command_name = args[0] + if command_name not in TASK_SESSION_COMMANDS: + return False + + subcommands = ticket.get("subcommands") + if not isinstance(subcommands, list): + return False + + for subcommand in subcommands: + if not isinstance(subcommand, dict): + continue + if _string_value(subcommand.get("name")) != command_name: + continue + if command_name != "start": + return True + task_ref = args[1] if len(args) > 1 else None + if _task_refs_match(_string_value(subcommand.get("task_ref")), task_ref, repo_root): + return True + + return False + + +def _ticket_is_fresh(ticket: dict[str, Any], ticket_path: Path, now: float) -> bool: + expires_at = ticket.get("expires_at_epoch") + if isinstance(expires_at, (int, float)) and expires_at < now: + _remove_file(ticket_path) + return False + + created_at = ticket.get("created_at_epoch") + if isinstance(created_at, (int, float)): + if now - created_at <= CURSOR_SHELL_TICKET_TTL_SECONDS: + return True + _remove_file(ticket_path) + return False + return True + + +def _ticket_cwd_matches_repo(ticket: dict[str, Any], repo_root: Path) -> bool: + cwd = _string_value(ticket.get("cwd")) + if not cwd: + return True + try: + Path(cwd).resolve().relative_to(repo_root) + except ValueError: + return False + return True + + +def _matching_cursor_ticket_context_key( + ticket_path: Path, + repo_root: Path, + now: float, +) -> str | None: + ticket = _read_json(ticket_path) + if ticket is None or ticket.get("platform") != "cursor": + return None + if not _ticket_is_fresh(ticket, ticket_path, now): + return None + if not _ticket_cwd_matches_repo(ticket, repo_root): + return None + if not _pending_ticket_matches_args(ticket, repo_root): + return None + return _string_value(ticket.get("context_key")) + + +def _lookup_cursor_shell_ticket_context_key() -> str | None: + """Resolve Cursor conversation identity from a short-lived shell ticket. + + Cursor exposes `conversation_id` to `beforeShellExecution`, but does not + export it into the shell command environment. The Cursor hook writes a + short-lived ticket just before `task.py` runs. We accept a ticket only when + the current `task.py` subcommand matches and exactly one fresh context key + matches, which avoids cross-window pointer contamination. + """ + repo_root = _find_repo_root_from_cwd() + if repo_root is None: + return None + + ticket_dir = _cursor_shell_ticket_dir(repo_root) + if not ticket_dir.is_dir(): + return None + + now = time.time() + candidates: set[str] = set() + for ticket_path in ticket_dir.glob("*.json"): + context_key = _matching_cursor_ticket_context_key(ticket_path, repo_root, now) + if context_key: + candidates.add(context_key) + + if len(candidates) == 1: + return next(iter(candidates)) + return None + + +def resolve_context_key( + platform_input: dict[str, Any] | None = None, + platform: str | None = None, +) -> str | None: + """Resolve a stable session/window context key, if one is available. + + `TRELLIS_CONTEXT_ID` is an explicit context-key override used by CLI + scripts and subprocesses. It does not store the task itself. + """ + override = _string_value(os.environ.get("TRELLIS_CONTEXT_ID")) + if override: + return _sanitize_key(override) or _hash_value(override) + + data = _as_dict(platform_input) + platform_name = _detect_platform(data, platform) if data or platform else None + + if data: + session_id = _lookup_string(data, _SESSION_KEYS) + if session_id: + return _context_key(platform_name or "session", "session", session_id) + + conversation_id = _lookup_string(data, _CONVERSATION_KEYS) + if conversation_id: + return _context_key(platform_name or "session", "conversation", conversation_id) + + transcript_path = _lookup_string(data, _TRANSCRIPT_KEYS) + if transcript_path: + return _context_key(platform_name or "session", "transcript", transcript_path) + + env_context_key = _lookup_env_context_key(platform_name) + if env_context_key: + return env_context_key + + if platform_name in (None, "session", "cursor"): + return _lookup_cursor_shell_ticket_context_key() + return None + + +def _read_json(path: Path) -> dict[str, Any] | None: + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (FileNotFoundError, json.JSONDecodeError, OSError): + return None + return data if isinstance(data, dict) else None + + +def _write_json(path: Path, data: dict[str, Any]) -> bool: + try: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(data, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + return True + except OSError: + return False + + +def _canonical_task_ref(task_path: str, repo_root: Path) -> str | None: + normalized = normalize_task_ref(task_path) + if not normalized: + return None + full_path = resolve_task_ref(normalized, repo_root) + if full_path is None or not full_path.is_dir(): + return None + try: + return full_path.relative_to(repo_root).as_posix() + except ValueError: + return str(full_path) + + +def _active_from_ref( + task_ref: str | None, + repo_root: Path, + source_type: str, + context_key: str | None = None, +) -> ActiveTask | None: + if not task_ref: + return None + resolved = resolve_task_ref(task_ref, repo_root) + stale = resolved is None or not resolved.is_dir() + return ActiveTask(task_ref, source_type, context_key, stale) + + +def _context_path(repo_root: Path, context_key: str) -> Path: + return _runtime_sessions_dir(repo_root) / f"{context_key}.json" + + +def resolve_active_task( + repo_root: Path, + platform_input: dict[str, Any] | None = None, + platform: str | None = None, +) -> ActiveTask: + """Resolve the active task from session runtime state only. + + A stale session task is returned as stale. Missing context identity or a + missing/empty session context falls back to single-session inference: if + exactly one session file exists in the runtime, return its task with + source_type="session-fallback" — covers class-2 platform sub-agents (codex, + copilot, gemini, qoder) that don't inherit the parent's session id. ≥2 + files or 0 files yield ActiveTask(None) — refuses to guess across windows. + """ + context_key = resolve_context_key(platform_input, platform) + if context_key: + context = _read_json(_context_path(repo_root, context_key)) or {} + task_ref = _string_value(context.get("current_task")) + active = _active_from_ref(task_ref, repo_root, "session", context_key) + if active: + return active + + fallback = _resolve_single_session_fallback(repo_root) + if fallback is not None: + return fallback + + return ActiveTask(None, "none", context_key) + + +def _resolve_single_session_fallback(repo_root: Path) -> ActiveTask | None: + """Return the task pointed at by the sole session file, if exactly one exists. + + Used when context-key resolution fails (typical for class-2 platform + sub-agents). Returns None if 0 or ≥2 session files are present — refuses + to pick across windows so 04-21's multi-session isolation contract holds. + """ + sessions_dir = _runtime_sessions_dir(repo_root) + if not sessions_dir.is_dir(): + return None + + session_files = sorted(sessions_dir.glob("*.json")) + if len(session_files) != 1: + return None + + session_file = session_files[0] + context = _read_json(session_file) or {} + task_ref = _string_value(context.get("current_task")) + if not task_ref: + return None + + fallback_key = session_file.stem + return _active_from_ref(task_ref, repo_root, "session-fallback", fallback_key) + + +def _utc_now() -> str: + return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def _context_metadata( + platform_input: dict[str, Any] | None, + platform: str | None, + context_key: str | None = None, +) -> dict[str, Any]: + data = _as_dict(platform_input) or {} + platform_name = _detect_platform(data, platform) + if platform_name == "session" and context_key: + prefix = context_key.split("_", 1)[0] + if prefix in _KNOWN_PLATFORMS: + platform_name = prefix + metadata: dict[str, Any] = { + "platform": platform_name, + "last_seen_at": _utc_now(), + } + for key in (*_SESSION_KEYS, *_CONVERSATION_KEYS, *_TRANSCRIPT_KEYS): + value = _lookup_string(data, (key,)) + if value: + metadata[key] = value + return metadata + + +def set_active_task( + task_path: str, + repo_root: Path, + platform_input: dict[str, Any] | None = None, + platform: str | None = None, +) -> ActiveTask | None: + """Set the active task in session scope. + + Returns None when no context key is available; callers should surface a + user-facing error that explains how to provide session identity. + """ + canonical = _canonical_task_ref(task_path, repo_root) + if canonical is None: + return None + + context_key = resolve_context_key(platform_input, platform) + if not context_key: + return None + + context_path = _context_path(repo_root, context_key) + context = _read_json(context_path) or {} + context.update(_context_metadata(platform_input, platform, context_key)) + context["current_task"] = canonical + context.setdefault("current_run", None) + if not _write_json(context_path, context): + return None + return ActiveTask(canonical, "session", context_key) + + +def clear_active_task( + repo_root: Path, + platform_input: dict[str, Any] | None = None, + platform: str | None = None, +) -> ActiveTask: + """Clear the active task by deleting the current session context file.""" + context_key = resolve_context_key(platform_input, platform) + if not context_key: + return ActiveTask(None, "none") + + previous = resolve_active_task(repo_root, platform_input, platform) + context_path = _context_path(repo_root, context_key) + if context_path.is_file(): + _remove_file(context_path) + return previous + + +def clear_task_from_sessions(task_path: str, repo_root: Path) -> int: + """Delete all session runtime files that point at a task.""" + target = _canonical_task_ref(task_path, repo_root) or normalize_task_ref(task_path) + if not target: + return 0 + + cleared = 0 + sessions_dir = _runtime_sessions_dir(repo_root) + if not sessions_dir.is_dir(): + return cleared + + for session_path in sessions_dir.glob("*.json"): + context = _read_json(session_path) or {} + current = _string_value(context.get("current_task")) + if not current: + continue + current_ref = _canonical_task_ref(current, repo_root) or normalize_task_ref(current) + if current_ref != target: + continue + if session_path.is_file() and _remove_file(session_path): + cleared += 1 + + return cleared + + +def get_current_task_source( + repo_root: Path, + platform_input: dict[str, Any] | None = None, + platform: str | None = None, +) -> tuple[str, str | None, str | None]: + """Return (`source_type`, `context_key`, `task_path`) for compatibility.""" + active = resolve_active_task(repo_root, platform_input, platform) + return active.source_type, active.context_key, active.task_path diff --git a/.trellis/scripts/common/cli_adapter.py b/.trellis/scripts/common/cli_adapter.py new file mode 100755 index 0000000..8d594a5 --- /dev/null +++ b/.trellis/scripts/common/cli_adapter.py @@ -0,0 +1,851 @@ +""" +CLI Adapter for Multi-Platform Support. + +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) +- opencode: OpenCode +- cursor: Cursor IDE +- iflow: iFlow CLI +- codex: Codex CLI (skills-based) +- kilo: Kilo CLI +- kiro: Kiro Code (skills-based) +- gemini: Gemini CLI +- antigravity: Antigravity (workflow-based) +- 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) + +Usage: + from common.cli_adapter import CLIAdapter + + adapter = CLIAdapter("opencode") + cmd = adapter.build_run_command( + agent="dispatch", + session_id="abc123", + prompt="Start the pipeline" + ) +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import ClassVar, Literal + +Platform = Literal[ + "claude", + "opencode", + "cursor", + "iflow", + "codex", + "kilo", + "kiro", + "gemini", + "antigravity", + "devin", + "qoder", + "codebuddy", + "copilot", + "droid", + "pi", + "trae", +] + + +@dataclass +class CLIAdapter: + """Adapter for different AI coding CLI tools.""" + + platform: Platform + + # ========================================================================= + # Agent Name Mapping + # ========================================================================= + + # OpenCode has built-in agents that cannot be overridden + # See: https://github.com/sst/opencode/issues/4271 + # Note: Class-level constant, not a dataclass field + _AGENT_NAME_MAP: ClassVar[dict[Platform, dict[str, str]]] = { + "claude": {}, # No mapping needed + "opencode": { + "plan": "trellis-plan", # 'plan' is built-in in OpenCode + }, + } + + def get_agent_name(self, agent: str) -> str: + """Get platform-specific agent name. + + Args: + agent: Original agent name (e.g., 'plan', 'dispatch') + + Returns: + Platform-specific agent name (e.g., 'trellis-plan' for OpenCode) + """ + mapping = self._AGENT_NAME_MAP.get(self.platform, {}) + return mapping.get(agent, agent) + + # ========================================================================= + # Agent Path + # ========================================================================= + + @property + def config_dir_name(self) -> str: + """Get platform-specific config directory name. + + Returns: + Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.devin', '.qoder', '.codebuddy', '.github/copilot', '.factory', '.pi', or '.trae') + """ + if self.platform == "opencode": + return ".opencode" + elif self.platform == "cursor": + return ".cursor" + elif self.platform == "iflow": + return ".iflow" + elif self.platform == "codex": + return ".codex" + elif self.platform == "kilo": + return ".kilocode" + elif self.platform == "kiro": + return ".kiro" + elif self.platform == "gemini": + return ".gemini" + elif self.platform == "antigravity": + return ".agent" + elif self.platform == "devin": + return ".devin" + elif self.platform == "qoder": + return ".qoder" + elif self.platform == "codebuddy": + return ".codebuddy" + elif self.platform == "copilot": + return ".github/copilot" + elif self.platform == "droid": + return ".factory" + elif self.platform == "pi": + return ".pi" + elif self.platform == "trae": + return ".trae" + else: + return ".claude" + + def get_config_dir(self, project_root: Path) -> Path: + """Get platform-specific config directory. + + Args: + project_root: Project root directory + + Returns: + Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .devin, .qoder, .codebuddy, .github/copilot, .factory, .pi, or .trae) + """ + return project_root / self.config_dir_name + + def get_agent_path(self, agent: str, project_root: Path) -> Path: + """Get path to agent definition file. + + Args: + agent: Agent name (original, before mapping) + project_root: Project root directory + + Returns: + Path to agent definition file (.md for most platforms, .toml for Codex) + """ + mapped_name = self.get_agent_name(agent) + if self.platform == "codex": + return self.get_config_dir(project_root) / "agents" / f"{mapped_name}.toml" + return self.get_config_dir(project_root) / "agents" / f"{mapped_name}.md" + + def get_commands_path(self, project_root: Path, *parts: str) -> Path: + """Get path to commands directory or specific command file. + + Args: + project_root: Project root directory + *parts: Additional path parts (e.g., 'trellis', 'finish-work.md') + + Returns: + Path to commands directory or file + + Note: + Cursor uses prefix naming: .cursor/commands/trellis-<name>.md + Antigravity uses workflow directory: .agent/workflows/<name>.md + 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 + """ + if self.platform == "pi": + prompts_dir = self.get_config_dir(project_root) / "prompts" + if not parts: + return prompts_dir + if len(parts) >= 2 and parts[0] == "trellis": + filename = parts[-1] + if filename.endswith(".md"): + filename = filename[:-3] + return prompts_dir / f"trellis-{filename}.md" + return prompts_dir / Path(*parts) + + if self.platform == "devin": + workflow_dir = self.get_config_dir(project_root) / "workflows" + if not parts: + return workflow_dir + if len(parts) >= 2 and parts[0] == "trellis": + filename = parts[-1] + return workflow_dir / f"trellis-{filename}" + return workflow_dir / Path(*parts) + + if self.platform in ("antigravity", "kilo"): + workflow_dir = self.get_config_dir(project_root) / "workflows" + if not parts: + return workflow_dir + if len(parts) >= 2 and parts[0] == "trellis": + filename = parts[-1] + return workflow_dir / filename + return workflow_dir / Path(*parts) + + if self.platform == "copilot": + prompts_dir = project_root / ".github" / "prompts" + if not parts: + return prompts_dir + if len(parts) >= 2 and parts[0] == "trellis": + filename = parts[-1] + if filename.endswith(".md"): + filename = filename[:-3] + return prompts_dir / f"{filename}.prompt.md" + return prompts_dir / Path(*parts) + + if not parts: + return self.get_config_dir(project_root) / "commands" + + # Cursor uses prefix naming instead of subdirectory + if self.platform == "cursor" and len(parts) >= 2 and parts[0] == "trellis": + # Convert trellis/<name>.md to trellis-<name>.md + filename = parts[-1] + return ( + self.get_config_dir(project_root) / "commands" / f"trellis-{filename}" + ) + + return self.get_config_dir(project_root) / "commands" / Path(*parts) + + def get_trellis_command_path(self, name: str) -> str: + """Get relative path to a trellis command file. + + Args: + name: Command name without extension (e.g., 'finish-work', 'check') + + Returns: + Relative path string for use in JSONL entries + + Note: + Cursor: .cursor/commands/trellis-<name>.md + Codex: .agents/skills/trellis-<name>/SKILL.md + Kiro: .kiro/skills/trellis-<name>/SKILL.md + Gemini: .gemini/commands/trellis/<name>.toml + Antigravity: .agent/workflows/<name>.md + Devin: .devin/workflows/trellis-<name>.md + Pi: .pi/prompts/trellis-<name>.md + Others: .{platform}/commands/trellis/<name>.md + """ + if self.platform == "cursor": + return f".cursor/commands/trellis-{name}.md" + elif self.platform == "codex": + # 0.5.0-beta.0 renamed all skill dirs to add the `trellis-` prefix + # (see that release's manifest for the 60+ rename entries). + return f".agents/skills/trellis-{name}/SKILL.md" + elif self.platform == "kiro": + return f".kiro/skills/trellis-{name}/SKILL.md" + elif self.platform == "gemini": + return f".gemini/commands/trellis/{name}.toml" + elif self.platform == "antigravity": + return f".agent/workflows/{name}.md" + elif self.platform == "devin": + return f".devin/workflows/trellis-{name}.md" + elif self.platform == "kilo": + return f".kilocode/workflows/{name}.md" + elif self.platform == "copilot": + return f".github/prompts/{name}.prompt.md" + elif self.platform == "droid": + return f".factory/commands/trellis/{name}.md" + elif self.platform == "pi": + return f".pi/prompts/trellis-{name}.md" + else: + return f"{self.config_dir_name}/commands/trellis/{name}.md" + + # ========================================================================= + # Environment Variables + # ========================================================================= + + def get_non_interactive_env(self) -> dict[str, str]: + """Get environment variables for non-interactive mode. + + Returns: + Dict of environment variables to set + """ + if self.platform == "opencode": + return {"OPENCODE_NON_INTERACTIVE": "1"} + elif self.platform == "iflow": + return {"IFLOW_NON_INTERACTIVE": "1"} + elif self.platform == "codex": + return {"CODEX_NON_INTERACTIVE": "1"} + elif self.platform == "kiro": + return {"KIRO_NON_INTERACTIVE": "1"} + elif self.platform == "gemini": + return {} # Gemini CLI doesn't have a non-interactive env var + elif self.platform == "antigravity": + return {} + elif self.platform == "devin": + return {} + elif self.platform == "qoder": + return {} + elif self.platform == "codebuddy": + return {} + elif self.platform == "copilot": + return {} + elif self.platform == "droid": + return {} + elif self.platform == "pi": + return {} + elif self.platform == "trae": + return {} + else: + return {"CLAUDE_NON_INTERACTIVE": "1"} + + # ========================================================================= + # CLI Command Building + # ========================================================================= + + def build_run_command( + self, + agent: str, + prompt: str, + session_id: str | None = None, + skip_permissions: bool = True, + verbose: bool = True, + json_output: bool = True, + ) -> list[str]: + """Build CLI command for running an agent. + + Args: + agent: Agent name (will be mapped if needed) + prompt: Prompt to send to the agent + session_id: Optional session ID (Claude Code only for creation) + skip_permissions: Whether to skip permission prompts + verbose: Whether to enable verbose output + json_output: Whether to use JSON output format + + Returns: + List of command arguments + """ + mapped_agent = self.get_agent_name(agent) + + if self.platform == "opencode": + cmd = ["opencode", "run"] + cmd.extend(["--agent", mapped_agent]) + + # Note: OpenCode 'run' mode is non-interactive by default + # No equivalent to Claude Code's --dangerously-skip-permissions + # See: https://github.com/anomalyco/opencode/issues/9070 + + if json_output: + cmd.extend(["--format", "json"]) + + if verbose: + cmd.extend(["--log-level", "DEBUG", "--print-logs"]) + + # Note: OpenCode doesn't support --session-id on creation + # Session ID must be extracted from logs after startup + + cmd.append(prompt) + + elif self.platform == "iflow": + cmd = ["iflow", "-y", "-p"] + cmd.append(f"${mapped_agent} {prompt}") + elif self.platform == "codex": + cmd = ["codex", "exec"] + cmd.append(prompt) + elif self.platform == "kiro": + cmd = ["kiro", "run", prompt] + elif self.platform == "gemini": + cmd = ["gemini"] + cmd.append(prompt) + elif self.platform == "antigravity": + raise ValueError( + "Antigravity workflows are UI slash commands; CLI agent run is not supported." + ) + elif self.platform == "devin": + raise ValueError( + "Devin workflows are UI slash commands; CLI agent run is not supported." + ) + elif self.platform == "qoder": + cmd = ["qodercli", "-p", prompt] + elif self.platform == "codebuddy": + raise ValueError( + "CodeBuddy does not support non-interactive mode (no CLI agent)" + ) + elif self.platform == "copilot": + raise ValueError( + "GitHub Copilot is IDE-only; CLI agent run is not supported." + ) + elif self.platform == "droid": + raise ValueError( + "Factory Droid CLI agent run is not yet supported." + ) + elif self.platform == "pi": + cmd = ["pi", "-p", prompt] + elif self.platform == "trae": + raise ValueError( + "Trae is IDE-only; CLI agent run is not supported." + ) + + else: # claude + cmd = ["claude", "-p"] + cmd.extend(["--agent", mapped_agent]) + + if session_id: + cmd.extend(["--session-id", session_id]) + + if skip_permissions: + cmd.append("--dangerously-skip-permissions") + + if json_output: + cmd.extend(["--output-format", "stream-json"]) + + if verbose: + cmd.append("--verbose") + + cmd.append(prompt) + + return cmd + + def build_resume_command(self, session_id: str) -> list[str]: + """Build CLI command for resuming a session. + + Args: + session_id: Session ID to resume (ignored for iFlow) + + Returns: + List of command arguments + """ + if self.platform == "opencode": + return ["opencode", "run", "--session", session_id] + elif self.platform == "iflow": + # iFlow uses -c to continue most recent conversation + # session_id is ignored as iFlow doesn't support session IDs + return ["iflow", "-c"] + elif self.platform == "codex": + return ["codex", "resume", session_id] + elif self.platform == "kiro": + return ["kiro", "resume", session_id] + elif self.platform == "gemini": + return ["gemini", "--resume", session_id] + elif self.platform == "antigravity": + raise ValueError( + "Antigravity workflows are UI slash commands; CLI resume is not supported." + ) + elif self.platform == "devin": + raise ValueError( + "Devin workflows are UI slash commands; CLI resume is not supported." + ) + elif self.platform == "qoder": + return ["qodercli", "--resume", session_id] + elif self.platform == "codebuddy": + raise ValueError( + "CodeBuddy does not support non-interactive mode (no CLI agent)" + ) + elif self.platform == "copilot": + raise ValueError( + "GitHub Copilot is IDE-only; CLI resume is not supported." + ) + elif self.platform == "droid": + raise ValueError( + "Factory Droid CLI resume is not yet supported." + ) + elif self.platform == "pi": + return ["pi", "-c", session_id] + elif self.platform == "trae": + raise ValueError( + "Trae is IDE-only; CLI resume is not supported." + ) + else: + return ["claude", "--resume", session_id] + + def get_resume_command_str(self, session_id: str, cwd: str | None = None) -> str: + """Get human-readable resume command string. + + Args: + session_id: Session ID to resume + cwd: Optional working directory to cd into + + Returns: + Command string for display + """ + cmd = self.build_resume_command(session_id) + cmd_str = " ".join(cmd) + + if cwd: + return f"cd {cwd} && {cmd_str}" + return cmd_str + + # ========================================================================= + # Platform Detection Helpers + # ========================================================================= + + @property + def is_opencode(self) -> bool: + """Check if platform is OpenCode.""" + return self.platform == "opencode" + + @property + def is_claude(self) -> bool: + """Check if platform is Claude Code.""" + return self.platform == "claude" + + @property + def is_cursor(self) -> bool: + """Check if platform is Cursor.""" + return self.platform == "cursor" + + @property + def is_iflow(self) -> bool: + """Check if platform is iFlow CLI.""" + return self.platform == "iflow" + + @property + def cli_name(self) -> str: + """Get CLI executable name. + + Note: Cursor doesn't have a CLI tool, returns None-like value. + """ + if self.is_opencode: + return "opencode" + elif self.is_cursor: + return "cursor" # Note: Cursor is IDE-only, no CLI + elif self.platform == "iflow": + return "iflow" + elif self.platform == "kiro": + return "kiro" + elif self.platform == "gemini": + return "gemini" + elif self.platform == "antigravity": + return "agy" + elif self.platform == "devin": + return "devin" + elif self.platform == "qoder": + return "qodercli" + elif self.platform == "codebuddy": + return "codebuddy" + elif self.platform == "copilot": + return "copilot" + elif self.platform == "droid": + return "droid" + elif self.platform == "pi": + return "pi" + elif self.platform == "trae": + return "trae" + else: + return "claude" + + @property + def supports_cli_agents(self) -> bool: + """Check if platform supports running agents via CLI. + + Claude Code, OpenCode, iFlow, and Codex support CLI agent execution. + Cursor is IDE-only and doesn't support CLI agents. + """ + return self.platform in ("claude", "opencode", "iflow", "codex", "pi") + + @property + def requires_agent_definition_file(self) -> bool: + """Check if platform requires an agent definition file (.md/.toml) to run. + + Claude Code, OpenCode, iFlow: require agent .md files (--agent flag). + Codex: auto-discovers agents from .codex/agents/*.toml, no --agent flag. + """ + return self.platform in ("claude", "opencode", "iflow") + + # ========================================================================= + # Session ID Handling + # ========================================================================= + + @property + def supports_session_id_on_create(self) -> bool: + """Check if platform supports specifying session ID on creation. + + Claude Code: Yes (--session-id) + OpenCode: No (auto-generated, extract from logs) + iFlow: No (no session ID support) + """ + return self.platform == "claude" + + def extract_session_id_from_log(self, log_content: str) -> str | None: + """Extract session ID from log output (OpenCode only). + + OpenCode generates session IDs in format: ses_xxx + + Args: + log_content: Log file content + + Returns: + Session ID if found, None otherwise + """ + import re + + # OpenCode session ID pattern + match = re.search(r"ses_[a-zA-Z0-9]+", log_content) + if match: + return match.group(0) + return None + + +# ============================================================================= +# Factory Function +# ============================================================================= + + +def get_cli_adapter(platform: str = "claude") -> CLIAdapter: + """Get CLI adapter for the specified platform. + + Args: + platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', '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", + "cursor", + "iflow", + "codex", + "kilo", + "kiro", + "gemini", + "antigravity", + "devin", + "qoder", + "codebuddy", + "copilot", + "droid", + "pi", + "trae", + ): + raise ValueError( + f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or 'trae')" + ) + + return CLIAdapter(platform=platform) # type: ignore + + +_ALL_PLATFORM_CONFIG_DIRS = ( + ".claude", + ".cursor", + ".iflow", + ".opencode", + ".codex", + ".kilocode", + ".kiro", + ".gemini", + ".agent", + ".devin", + ".windsurf", # deprecated: pre-rename Devin config dir (still a platform signal) + ".qoder", + ".codebuddy", + ".github/copilot", + ".factory", + ".pi", + ".trae", +) +"""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, Devin, or other platforms.""" + + +def _has_other_platform_dir(project_root: Path, exclude: set[str]) -> bool: + """Check if any platform config dir exists besides those in *exclude*.""" + return any( + (project_root / d).is_dir() + for d in _ALL_PLATFORM_CONFIG_DIRS + if d not in exclude + ) + + +def detect_platform(project_root: Path) -> Platform: + """Auto-detect platform based on existing config directories. + + Detection order: + 1. TRELLIS_PLATFORM environment variable (if set) + 2. .opencode directory exists → opencode + 3. .iflow directory exists → iflow + 4. .cursor directory exists (without .claude) → cursor + 5. .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. .devin/workflows (or legacy .windsurf/workflows) exists and no other platform dirs → devin + 11. .codebuddy directory exists → codebuddy + 12. .qoder directory exists → qoder + 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', '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", + "cursor", + "iflow", + "codex", + "kilo", + "kiro", + "gemini", + "antigravity", + "devin", + "qoder", + "codebuddy", + "copilot", + "droid", + "pi", + "trae", + ): + return env_platform # type: ignore + + # Check for .opencode directory (OpenCode-specific) + if (project_root / ".opencode").is_dir(): + return "opencode" + + # Check for .iflow directory (iFlow-specific) + if (project_root / ".iflow").is_dir(): + return "iflow" + + # Check for .cursor directory (Cursor-specific) + # Only detect as cursor if .claude doesn't exist (to avoid confusion) + if (project_root / ".cursor").is_dir() and not (project_root / ".claude").is_dir(): + return "cursor" + + # Check for .gemini directory (Gemini CLI-specific) + if (project_root / ".gemini").is_dir(): + return "gemini" + + # Check for .codex directory (Codex-specific) + # .agents/skills/ alone does NOT trigger codex detection (it's a shared standard) + if (project_root / ".codex").is_dir() and not _has_other_platform_dir( + project_root, {".codex", ".agents"} + ): + return "codex" + + # Check for .kilocode directory (Kilo-specific) + if (project_root / ".kilocode").is_dir(): + return "kilo" + + # Check for Kiro skills directory only when no other platform config exists + if (project_root / ".kiro" / "skills").is_dir() and not _has_other_platform_dir( + project_root, {".kiro"} + ): + return "kiro" + + # Check for Antigravity workflow directory only when no other platform config exists + if ( + project_root / ".agent" / "workflows" + ).is_dir() and not _has_other_platform_dir( + project_root, {".agent", ".gemini"} + ): + return "antigravity" + + # Check for 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 / ".devin" / "workflows").is_dir() + or (project_root / ".windsurf" / "workflows").is_dir() + ) and not _has_other_platform_dir( + project_root, {".devin", ".windsurf"} + ): + return "devin" + + # Check for .codebuddy directory (CodeBuddy-specific) + if (project_root / ".codebuddy").is_dir(): + return "codebuddy" + + # Check for .qoder directory (Qoder-specific) + if (project_root / ".qoder").is_dir(): + return "qoder" + + # Check for .github/copilot directory (GitHub Copilot-specific) + if (project_root / ".github" / "copilot").is_dir(): + return "copilot" + + # Check for .factory directory (Factory Droid-specific) + if (project_root / ".factory").is_dir(): + return "droid" + + # Check for .pi directory (Pi Agent-specific) + if (project_root / ".pi").is_dir(): + return "pi" + + # Check for .trae directory (Trae IDE-specific) + if (project_root / ".trae").is_dir(): + return "trae" + + # Fallback: checkout only has the Codex shared-skills layer + # (.agents/skills/trellis-* dirs) and no explicit platform config dir. + # Happens on fresh clones where .codex/ is gitignored/absent but the + # shared skills were committed to git. Must guard against the case + # where .claude/ or any other platform dir also exists — .agents/skills/ + # can legitimately coexist with any platform as a shared consumption + # layer for Amp/Cline/Warp/etc. + agents_skills = project_root / ".agents" / "skills" + if agents_skills.is_dir() and not _has_other_platform_dir( + project_root, set() + ): + try: + for entry in agents_skills.iterdir(): + if entry.is_dir() and entry.name.startswith("trellis-"): + return "codex" + except OSError: + pass + + return "claude" + + +def get_cli_adapter_auto(project_root: Path) -> CLIAdapter: + """Get CLI adapter with auto-detected platform. + + Args: + project_root: Project root directory + + Returns: + CLIAdapter instance for detected platform + """ + platform = detect_platform(project_root) + return CLIAdapter(platform=platform) diff --git a/.trellis/scripts/common/config.py b/.trellis/scripts/common/config.py new file mode 100755 index 0000000..93df643 --- /dev/null +++ b/.trellis/scripts/common/config.py @@ -0,0 +1,445 @@ +#!/usr/bin/env python3 +""" +Trellis configuration reader. + +Reads settings from .trellis/config.yaml with sensible defaults. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from .paths import DIR_WORKFLOW, get_repo_root + + +# ============================================================================= +# YAML Simple Parser (no dependencies) +# ============================================================================= + + +def _unquote(s: str) -> str: + """Remove exactly one layer of matching surrounding quotes. + + Unlike str.strip('"'), this only removes the outermost pair, + preserving any nested quotes inside the value. + + Examples: + _unquote('"hello"') -> 'hello' + _unquote("'hello'") -> 'hello' + _unquote('"echo \\'hi\\'"') -> "echo 'hi'" + _unquote('hello') -> 'hello' + _unquote('"hello\\'') -> '"hello\\'' (mismatched, unchanged) + """ + if len(s) >= 2 and s[0] == s[-1] and s[0] in ('"', "'"): + return s[1:-1] + return s + + +def _strip_inline_comment(value: str) -> str: + """Strip ` # …` inline comments while preserving `#` inside quoted strings. + + YAML treats ` #` (space-hash) as a comment opener; bare `#` inside a token + is part of the value. Quoted strings are immune. + + Mirrors :func:`common.trellis_config._strip_inline_comment` so both + parsers handle ``key: value # comment`` identically. + """ + in_quote: str | None = None + for idx, ch in enumerate(value): + if in_quote: + if ch == in_quote: + in_quote = None + continue + if ch in ('"', "'"): + in_quote = ch + continue + if ch == "#" and (idx == 0 or value[idx - 1].isspace()): + return value[:idx] + return value + + +def parse_simple_yaml(content: str) -> dict: + """Parse simple YAML with nested dict support (no dependencies). + + Supports: + - key: value (string) + - key: (followed by list items) + - item1 + - item2 + - key: (followed by nested dict) + nested_key: value + nested_key2: + - item + + Uses indentation to detect nesting (2+ spaces deeper = child). + + Args: + content: YAML content string. + + Returns: + Parsed dict (values can be str, list[str], or dict). + """ + lines = content.splitlines() + result: dict = {} + _parse_yaml_block(lines, 0, 0, result) + return result + + +def _parse_yaml_block( + lines: list[str], start: int, min_indent: int, target: dict +) -> int: + """Parse a YAML block into target dict, returning next line index.""" + i = start + current_list: list | None = None + + while i < len(lines): + line = lines[i] + stripped = line.strip() + + # Skip empty lines and comments + if not stripped or stripped.startswith("#"): + i += 1 + continue + + # Calculate indentation + indent = len(line) - len(line.lstrip()) + + # If dedented past our block, we're done + if indent < min_indent: + break + + if stripped.startswith("- "): + if current_list is not None: + current_list.append(_unquote(stripped[2:].strip())) + i += 1 + elif ":" in stripped: + key, _, value = stripped.partition(":") + key = key.strip() + value = _strip_inline_comment(value).strip() + value = _unquote(value) + current_list = None + + if value: + # key: value + target[key] = value + i += 1 + else: + # key: (no value) — peek ahead to determine list vs nested dict + next_i, next_line = _next_content_line(lines, i + 1) + if next_i >= len(lines): + target[key] = {} + i = next_i + elif next_line.strip().startswith("- "): + # It's a list + current_list = [] + target[key] = current_list + i += 1 + else: + next_indent = len(next_line) - len(next_line.lstrip()) + if next_indent > indent: + # It's a nested dict + nested: dict = {} + target[key] = nested + i = _parse_yaml_block(lines, i + 1, next_indent, nested) + else: + # Empty value, same or less indent follows + target[key] = {} + i += 1 + else: + i += 1 + + return i + + +def _next_content_line(lines: list[str], start: int) -> tuple[int, str]: + """Find the next non-empty, non-comment line.""" + i = start + while i < len(lines): + stripped = lines[i].strip() + if stripped and not stripped.startswith("#"): + return i, lines[i] + i += 1 + return i, "" + + +# Defaults +DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal" +DEFAULT_MAX_JOURNAL_LINES = 2000 +DEFAULT_SESSION_AUTO_COMMIT = True + +CONFIG_FILE = "config.yaml" + + +def _is_true_config_value(value: object) -> bool: + """Return True when a config value represents an enabled flag.""" + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.strip().lower() == "true" + return False + + +def _get_config_path(repo_root: Path | None = None) -> Path: + """Get path to config.yaml.""" + root = repo_root or get_repo_root() + return root / DIR_WORKFLOW / CONFIG_FILE + + +def _load_config(repo_root: Path | None = None) -> dict: + """Load and parse config.yaml. Returns empty dict on any error.""" + config_file = _get_config_path(repo_root) + try: + content = config_file.read_text(encoding="utf-8") + return parse_simple_yaml(content) + except (OSError, IOError): + return {} + + +def get_session_commit_message(repo_root: Path | None = None) -> str: + """Get the commit message for auto-committing session records.""" + config = _load_config(repo_root) + return config.get("session_commit_message", DEFAULT_SESSION_COMMIT_MESSAGE) + + +def get_max_journal_lines(repo_root: Path | None = None) -> int: + """Get the maximum lines per journal file.""" + config = _load_config(repo_root) + value = config.get("max_journal_lines", DEFAULT_MAX_JOURNAL_LINES) + try: + return int(value) + except (ValueError, TypeError): + return DEFAULT_MAX_JOURNAL_LINES + + +def get_session_auto_commit(repo_root: Path | None = None) -> bool: + """Whether scripts should auto-stage + auto-commit session/task changes. + + Governs both ``add_session.py:_auto_commit_workspace`` and + ``task_store.py:_auto_commit_archive``. + + Default: ``True`` (existing behavior — auto-stage + auto-commit). + Set ``session_auto_commit: false`` in ``.trellis/config.yaml`` to skip + auto-staging entirely; the journal/archive files are still written to + disk, but the user manages ``git add`` / ``git commit`` themselves. + + Accepts native YAML booleans (``true`` / ``false``) and the string + aliases ``true / false / yes / no / 1 / 0 / on / off`` (case-insensitive). + Invalid values fall back to ``True`` with a stderr warning. + """ + config = _load_config(repo_root) + raw = config.get("session_auto_commit", DEFAULT_SESSION_AUTO_COMMIT) + if isinstance(raw, bool): + return raw + s = str(raw).strip().lower() + if s in ("true", "yes", "1", "on"): + return True + if s in ("false", "no", "0", "off"): + return False + print( + f"[WARN] invalid session_auto_commit value: {raw!r}; using true (default)", + file=sys.stderr, + ) + return DEFAULT_SESSION_AUTO_COMMIT + + +def get_hooks(event: str, repo_root: Path | None = None) -> list[str]: + """Get hook commands for a lifecycle event. + + Args: + event: Event name (e.g. "after_create", "after_archive"). + repo_root: Repository root path. + + Returns: + List of shell commands to execute, empty if none configured. + """ + config = _load_config(repo_root) + hooks = config.get("hooks") + if not isinstance(hooks, dict): + return [] + commands = hooks.get(event) + if isinstance(commands, list): + return [str(c) for c in commands] + return [] + + +# ============================================================================= +# Monorepo / Packages +# ============================================================================= + + +def get_packages(repo_root: Path | None = None) -> dict[str, dict] | None: + """Get monorepo package declarations. + + Returns: + Dict mapping package name to its config (path, type, etc.), + or None if not configured (single-repo mode). + + Example return: + {"cli": {"path": "packages/cli"}, "docs-site": {"path": "docs-site", "type": "submodule"}} + """ + config = _load_config(repo_root) + packages = config.get("packages") + if not isinstance(packages, dict): + return None + # Ensure each value is a dict (filter out scalar entries) + filtered = {k: v for k, v in packages.items() if isinstance(v, dict)} + if not filtered: + return None + return filtered + + +def get_default_package(repo_root: Path | None = None) -> str | None: + """Get the default package name from config. + + Returns: + Package name string, or None if not configured. + """ + config = _load_config(repo_root) + value = config.get("default_package") + return str(value) if value else None + + +def get_submodule_packages(repo_root: Path | None = None) -> dict[str, str]: + """Get packages that are git submodules. + + Returns: + Dict mapping package name to its path for submodule-type packages. + Empty dict if none configured. + + Example return: + {"docs-site": "docs-site"} + """ + packages = get_packages(repo_root) + if packages is None: + return {} + return { + name: cfg.get("path", name) + for name, cfg in packages.items() + if cfg.get("type") == "submodule" + } + + +def get_git_packages(repo_root: Path | None = None) -> dict[str, str]: + """Get packages that have their own independent git repository. + + These are sub-directories with their own .git (not submodules), + marked with ``git: true`` in config.yaml. + + Returns: + Dict mapping package name to its path for git-repo packages. + Empty dict if none configured. + + Example config:: + + packages: + backend: + path: iqs + git: true + + Example return:: + + {"backend": "iqs"} + """ + packages = get_packages(repo_root) + if packages is None: + return {} + return { + name: cfg.get("path", name) + for name, cfg in packages.items() + if _is_true_config_value(cfg.get("git")) + } + + +def is_monorepo(repo_root: Path | None = None) -> bool: + """Check if the project is configured as a monorepo (has packages in config).""" + return get_packages(repo_root) is not None + + +def get_spec_base(package: str | None = None, repo_root: Path | None = None) -> str: + """Get the spec directory base path relative to .trellis/. + + Single-repo: returns "spec" + Monorepo with package: returns "spec/<package>" + Monorepo without package: returns "spec" (caller should specify package) + """ + if package and is_monorepo(repo_root): + return f"spec/{package}" + return "spec" + + +def validate_package(package: str, repo_root: Path | None = None) -> bool: + """Check if a package name is valid in this project. + + Single-repo (no packages configured): always returns True. + Monorepo: returns True only if package exists in config.yaml packages. + """ + packages = get_packages(repo_root) + if packages is None: + return True # Single-repo, no validation needed + return package in packages + + +def resolve_package( + task_package: str | None = None, + repo_root: Path | None = None, +) -> str | None: + """Resolve package from inferred sources with validation. + + Checks in order: task_package → default_package. + Invalid inferred values print a warning to stderr and are skipped. + + Returns: + Resolved package name, or None if no valid package found. + + Note: + CLI --package should be validated separately by the caller + (fail-fast with available packages list on error). + """ + packages = get_packages(repo_root) + if packages is None: + return None # Single-repo, no package needed + + # Try task_package (guard against non-string values from malformed JSON) + if task_package and isinstance(task_package, str): + if task_package in packages: + return task_package + print( + f"Warning: task.json package '{task_package}' not found in config, skipping", + file=sys.stderr, + ) + + # Try default_package + default = get_default_package(repo_root) + if default: + if default in packages: + return default + print( + f"Warning: default_package '{default}' not found in config, skipping", + file=sys.stderr, + ) + + return None + + +def get_spec_scope(repo_root: Path | None = None) -> list[str] | str | None: + """Get session.spec_scope configuration. + + Returns: + list[str]: Package names to include in spec scanning. + str: "active_task" to use current task's package. + None: No scope configured (scan all packages). + """ + config = _load_config(repo_root) + session = config.get("session") + if not isinstance(session, dict): + return None + + scope = session.get("spec_scope") + if scope is None: + return None + if isinstance(scope, str): + return scope # e.g. "active_task" + if isinstance(scope, list): + return [str(s) for s in scope] + return None diff --git a/.trellis/scripts/common/developer.py b/.trellis/scripts/common/developer.py new file mode 100755 index 0000000..c203a31 --- /dev/null +++ b/.trellis/scripts/common/developer.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +Developer management utilities. + +Provides: + init_developer - Initialize developer + ensure_developer - Ensure developer is initialized (exit if not) + show_developer_info - Show developer information +""" + +from __future__ import annotations + +import sys +from datetime import datetime +from pathlib import Path + +from .paths import ( + DIR_WORKFLOW, + DIR_WORKSPACE, + DIR_TASKS, + FILE_DEVELOPER, + FILE_JOURNAL_PREFIX, + get_repo_root, + get_developer, + check_developer, +) + + +# ============================================================================= +# Developer Initialization +# ============================================================================= + +def init_developer(name: str, repo_root: Path | None = None) -> bool: + """Initialize developer. + + Creates: + - .trellis/.developer file with developer info + - .trellis/workspace/<name>/ directory structure + - Initial journal file and index.md + + Args: + name: Developer name. + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + True on success, False on error. + """ + if not name: + print("Error: developer name is required", file=sys.stderr) + return False + + if repo_root is None: + repo_root = get_repo_root() + + dev_file = repo_root / DIR_WORKFLOW / FILE_DEVELOPER + workspace_dir = repo_root / DIR_WORKFLOW / DIR_WORKSPACE / name + + # Create .developer file + initialized_at = datetime.now().isoformat() + try: + dev_file.write_text( + f"name={name}\ninitialized_at={initialized_at}\n", + encoding="utf-8" + ) + except (OSError, IOError) as e: + print(f"Error: Failed to create .developer file: {e}", file=sys.stderr) + return False + + # Create workspace directory structure + try: + workspace_dir.mkdir(parents=True, exist_ok=True) + except (OSError, IOError) as e: + print(f"Error: Failed to create workspace directory: {e}", file=sys.stderr) + return False + + # Create initial journal file + journal_file = workspace_dir / f"{FILE_JOURNAL_PREFIX}1.md" + if not journal_file.exists(): + today = datetime.now().strftime("%Y-%m-%d") + journal_content = f"""# Journal - {name} (Part 1) + +> AI development session journal +> Started: {today} + +--- + +""" + try: + journal_file.write_text(journal_content, encoding="utf-8") + except (OSError, IOError) as e: + print(f"Error: Failed to create journal file: {e}", file=sys.stderr) + return False + + # Create index.md with markers for auto-update + index_file = workspace_dir / "index.md" + if not index_file.exists(): + index_content = f"""# Workspace Index - {name} + +> Journal tracking for AI development sessions. + +--- + +## Current Status + +<!-- @@@auto:current-status --> +- **Active File**: `journal-1.md` +- **Total Sessions**: 0 +- **Last Active**: - +<!-- @@@/auto:current-status --> + +--- + +## Active Documents + +<!-- @@@auto:active-documents --> +| File | Lines | Status | +|------|-------|--------| +| `journal-1.md` | ~0 | Active | +<!-- @@@/auto:active-documents --> + +--- + +## Session History + +<!-- @@@auto:session-history --> +| # | Date | Title | Commits | Branch | +|---|------|-------|---------|--------| +<!-- @@@/auto:session-history --> + +--- + +## Notes + +- Sessions are appended to journal files +- New journal file created when current exceeds 2000 lines +- Use `add_session.py` to record sessions +""" + try: + index_file.write_text(index_content, encoding="utf-8") + except (OSError, IOError) as e: + print(f"Error: Failed to create index.md: {e}", file=sys.stderr) + return False + + print(f"Developer initialized: {name}") + print(f" .developer file: {dev_file}") + print(f" Workspace dir: {workspace_dir}") + + return True + + +def ensure_developer(repo_root: Path | None = None) -> None: + """Ensure developer is initialized, exit if not. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + """ + if repo_root is None: + repo_root = get_repo_root() + + if not check_developer(repo_root): + print("Error: Developer not initialized.", file=sys.stderr) + print(f"Run: python3 ./{DIR_WORKFLOW}/scripts/init_developer.py <your-name>", file=sys.stderr) + sys.exit(1) + + +def show_developer_info(repo_root: Path | None = None) -> None: + """Show developer information. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + """ + if repo_root is None: + repo_root = get_repo_root() + + developer = get_developer(repo_root) + + if not developer: + print("Developer: (not initialized)") + else: + print(f"Developer: {developer}") + print(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/") + print(f"Tasks: {DIR_WORKFLOW}/{DIR_TASKS}/") + + +# ============================================================================= +# Main Entry (for testing) +# ============================================================================= + +if __name__ == "__main__": + show_developer_info() diff --git a/.trellis/scripts/common/git.py b/.trellis/scripts/common/git.py new file mode 100755 index 0000000..c4bf29f --- /dev/null +++ b/.trellis/scripts/common/git.py @@ -0,0 +1,31 @@ +""" +Git command execution utility. + +Single source of truth for running git commands across all Trellis scripts. +""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + + +def run_git(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]: + """Run a git command and return (returncode, stdout, stderr). + + Uses UTF-8 encoding with -c i18n.logOutputEncoding=UTF-8 to ensure + consistent output across all platforms (Windows, macOS, Linux). + """ + try: + git_args = ["git", "-c", "i18n.logOutputEncoding=UTF-8"] + args + result = subprocess.run( + git_args, + cwd=cwd, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + return result.returncode, result.stdout, result.stderr + except Exception as e: + return 1, "", str(e) diff --git a/.trellis/scripts/common/git_context.py b/.trellis/scripts/common/git_context.py new file mode 100755 index 0000000..23fc6ec --- /dev/null +++ b/.trellis/scripts/common/git_context.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Git and Session Context utilities. + +Entry shim — delegates to session_context and packages_context. + +Provides: + output_json - Output context in JSON format + output_text - Output context in text format +""" + +from __future__ import annotations + +import json + +from .git import run_git +from .session_context import ( + get_context_json, + get_context_text, + get_context_record_json, + get_context_text_record, + output_json, + output_text, +) +from .packages_context import ( + get_context_packages_text, + get_context_packages_json, +) +from .trellis_config import read_trellis_config +from .workflow_phase import ( + filter_platform, + get_phase_index, + get_step, + resolve_effective_platform, +) + +# Backward-compatible alias — external modules import this name +_run_git_command = run_git + + +# ============================================================================= +# Main Entry +# ============================================================================= + +def main() -> None: + """CLI entry point.""" + import argparse + + parser = argparse.ArgumentParser(description="Get Session Context for AI Agent") + parser.add_argument( + "--json", + "-j", + action="store_true", + help="Output in JSON format (works with any --mode)", + ) + parser.add_argument( + "--mode", + "-m", + choices=["default", "record", "packages", "phase"], + default="default", + help="Output mode: default (full context), record (for record-session), packages (package info only), phase (workflow step extraction)", + ) + parser.add_argument( + "--step", + help="Step id for --mode phase, e.g. 1.1, 2.2. Omit to get the Phase Index.", + ) + parser.add_argument( + "--platform", + help="Platform name for --mode phase, e.g. cursor, claude-code. Filters platform-tagged blocks.", + ) + + args = parser.parse_args() + + if args.mode == "record": + if args.json: + print(json.dumps(get_context_record_json(), indent=2, ensure_ascii=False)) + else: + print(get_context_text_record()) + elif args.mode == "packages": + if args.json: + print(json.dumps(get_context_packages_json(), indent=2, ensure_ascii=False)) + else: + print(get_context_packages_text()) + elif args.mode == "phase": + content = get_step(args.step) if args.step else get_phase_index() + if not content.strip(): + if args.step: + parser.exit(2, f"Step not found: {args.step}\n") + else: + parser.exit(2, "Phase Index section not found in workflow.md\n") + if args.platform: + effective = resolve_effective_platform( + args.platform, read_trellis_config() + ) + content = filter_platform(content, effective) + print(content, end="") + else: + if args.json: + output_json() + else: + output_text() + + +if __name__ == "__main__": + main() diff --git a/.trellis/scripts/common/io.py b/.trellis/scripts/common/io.py new file mode 100755 index 0000000..44288f4 --- /dev/null +++ b/.trellis/scripts/common/io.py @@ -0,0 +1,37 @@ +""" +JSON file I/O utilities. + +Provides read_json and write_json as the single source of truth +for JSON file operations across all Trellis scripts. +""" + +from __future__ import annotations + +import json +from pathlib import Path + + +def read_json(path: Path) -> dict | None: + """Read and parse a JSON file. + + Returns None if the file doesn't exist, is invalid JSON, or can't be read. + """ + try: + return json.loads(path.read_text(encoding="utf-8")) + except (FileNotFoundError, json.JSONDecodeError, OSError): + return None + + +def write_json(path: Path, data: dict) -> bool: + """Write dict to JSON file with pretty formatting. + + Returns True on success, False on error. + """ + try: + path.write_text( + json.dumps(data, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + return True + except (OSError, IOError): + return False diff --git a/.trellis/scripts/common/log.py b/.trellis/scripts/common/log.py new file mode 100755 index 0000000..839c643 --- /dev/null +++ b/.trellis/scripts/common/log.py @@ -0,0 +1,45 @@ +""" +Terminal output utilities: colors and structured logging. + +Single source of truth for Colors and log_* functions +used across all Trellis scripts. +""" + +from __future__ import annotations + + +class Colors: + """ANSI color codes for terminal output.""" + + RED = "\033[0;31m" + GREEN = "\033[0;32m" + YELLOW = "\033[1;33m" + BLUE = "\033[0;34m" + CYAN = "\033[0;36m" + DIM = "\033[2m" + NC = "\033[0m" # No Color / Reset + + +def colored(text: str, color: str) -> str: + """Apply ANSI color to text.""" + return f"{color}{text}{Colors.NC}" + + +def log_info(msg: str) -> None: + """Print info-level message with [INFO] prefix.""" + print(f"{Colors.BLUE}[INFO]{Colors.NC} {msg}") + + +def log_success(msg: str) -> None: + """Print success message with [SUCCESS] prefix.""" + print(f"{Colors.GREEN}[SUCCESS]{Colors.NC} {msg}") + + +def log_warn(msg: str) -> None: + """Print warning message with [WARN] prefix.""" + print(f"{Colors.YELLOW}[WARN]{Colors.NC} {msg}") + + +def log_error(msg: str) -> None: + """Print error message with [ERROR] prefix.""" + print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}") diff --git a/.trellis/scripts/common/packages_context.py b/.trellis/scripts/common/packages_context.py new file mode 100755 index 0000000..e7d4e8c --- /dev/null +++ b/.trellis/scripts/common/packages_context.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +""" +Package discovery and context output. + +Provides: + get_packages_info - Get structured package info + get_packages_section - Build PACKAGES text section + get_context_packages_text - Full packages text output (--mode packages) + get_context_packages_json - Full packages JSON output (--mode packages --json) +""" + +from __future__ import annotations + +from pathlib import Path + +from .config import _is_true_config_value, get_default_package, get_packages, get_spec_scope +from .paths import ( + DIR_SPEC, + DIR_WORKFLOW, + get_current_task, + get_repo_root, +) +from .tasks import load_task + + +# ============================================================================= +# Internal Helpers +# ============================================================================= + +def _scan_spec_layers(spec_dir: Path, package: str | None = None) -> list[str]: + """Scan spec directory for available layers (subdirectories). + + For monorepo: scans spec/<package>/ + For single-repo: scans spec/ + """ + target = spec_dir / package if package else spec_dir + if not target.is_dir(): + return [] + return sorted( + d.name for d in target.iterdir() if d.is_dir() and d.name != "guides" + ) + + +def _get_active_task_package(repo_root: Path) -> str | None: + """Get the package field from the active task's task.json.""" + current = get_current_task(repo_root) + if not current: + return None + ct = load_task(repo_root / current) + return ct.package if ct and ct.package else None + + +def _resolve_scope_set( + packages: dict, + spec_scope, + task_pkg: str | None, + default_pkg: str | None, +) -> set | None: + """Resolve spec_scope to a set of allowed package names, or None for full scan.""" + if not packages: + return None + + if spec_scope is None: + return None + + if isinstance(spec_scope, str) and spec_scope == "active_task": + if task_pkg and task_pkg in packages: + return {task_pkg} + if default_pkg and default_pkg in packages: + return {default_pkg} + return None + + if isinstance(spec_scope, list): + valid = {e for e in spec_scope if e in packages} + if valid: + return valid + # All invalid: fallback + if task_pkg and task_pkg in packages: + return {task_pkg} + if default_pkg and default_pkg in packages: + return {default_pkg} + return None + + return None + + +# ============================================================================= +# Public Functions +# ============================================================================= + +def get_packages_info(repo_root: Path) -> list[dict]: + """Get structured package info for monorepo projects. + + Returns list of dicts with keys: name, path, type, default, specLayers, + isSubmodule, isGitRepo. + Returns empty list for single-repo projects. + """ + packages = get_packages(repo_root) + if not packages: + return [] + + default_pkg = get_default_package(repo_root) + spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC + result = [] + + for pkg_name, pkg_config in packages.items(): + pkg_path = pkg_config.get("path", pkg_name) if isinstance(pkg_config, dict) else str(pkg_config) + pkg_type = pkg_config.get("type", "local") if isinstance(pkg_config, dict) else "local" + pkg_git = pkg_config.get("git", False) if isinstance(pkg_config, dict) else False + layers = _scan_spec_layers(spec_dir, pkg_name) + + result.append({ + "name": pkg_name, + "path": pkg_path, + "type": pkg_type, + "default": pkg_name == default_pkg, + "specLayers": layers, + "isSubmodule": pkg_type == "submodule", + "isGitRepo": _is_true_config_value(pkg_git), + }) + + return result + + +def get_packages_section(repo_root: Path) -> str: + """Build the PACKAGES section for text output.""" + spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC + pkg_info = get_packages_info(repo_root) + + lines: list[str] = [] + lines.append("## PACKAGES") + + if not pkg_info: + lines.append("(single-repo mode)") + layers = _scan_spec_layers(spec_dir) + if layers: + lines.append(f"Spec layers: {', '.join(layers)}") + return "\n".join(lines) + + default_pkg = get_default_package(repo_root) + + for pkg in pkg_info: + layers_str = f" [{', '.join(pkg['specLayers'])}]" if pkg["specLayers"] else "" + submodule_tag = " (submodule)" if pkg["isSubmodule"] else "" + git_repo_tag = " (git repo)" if pkg["isGitRepo"] else "" + default_tag = " *" if pkg["default"] else "" + lines.append( + f"- {pkg['name']:<16} {pkg['path']:<20}{layers_str}{submodule_tag}{git_repo_tag}{default_tag}" + ) + + if default_pkg: + lines.append(f"Default package: {default_pkg}") + + return "\n".join(lines) + + +def get_context_packages_text(repo_root: Path | None = None) -> str: + """Get packages context as formatted text (for --mode packages).""" + if repo_root is None: + repo_root = get_repo_root() + + pkg_info = get_packages_info(repo_root) + lines: list[str] = [] + + if not pkg_info: + spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC + lines.append("Single-repo project (no packages configured)") + lines.append("") + layers = _scan_spec_layers(spec_dir) + if layers: + lines.append(f"Spec layers: {', '.join(layers)}") + return "\n".join(lines) + + # Resolve scope for annotations + packages_dict = get_packages(repo_root) or {} + default_pkg = get_default_package(repo_root) + spec_scope = get_spec_scope(repo_root) + task_pkg = _get_active_task_package(repo_root) + scope_set = _resolve_scope_set(packages_dict, spec_scope, task_pkg, default_pkg) + + lines.append("## PACKAGES") + lines.append("") + for pkg in pkg_info: + default_tag = " (default)" if pkg["default"] else "" + type_tag = f" [{pkg['type']}]" if pkg["type"] != "local" else "" + git_tag = " [git repo]" if pkg["isGitRepo"] else "" + + # Scope annotation + scope_tag = "" + if scope_set is not None and pkg["name"] not in scope_set: + scope_tag = " (out of scope)" + + lines.append(f"### {pkg['name']}{default_tag}{type_tag}{git_tag}{scope_tag}") + lines.append(f"Path: {pkg['path']}") + if pkg["specLayers"]: + lines.append(f"Spec layers: {', '.join(pkg['specLayers'])}") + for layer in pkg["specLayers"]: + lines.append(f" - .trellis/spec/{pkg['name']}/{layer}/index.md") + else: + lines.append("Spec: not configured") + lines.append("") + + # Also show shared guides + guides_dir = repo_root / DIR_WORKFLOW / DIR_SPEC / "guides" + if guides_dir.is_dir(): + lines.append("### Shared Guides (always included)") + lines.append("Path: .trellis/spec/guides/index.md") + lines.append("") + + return "\n".join(lines) + + +def get_context_packages_json(repo_root: Path | None = None) -> dict: + """Get packages context as a dictionary (for --mode packages --json).""" + if repo_root is None: + repo_root = get_repo_root() + + pkg_info = get_packages_info(repo_root) + + if not pkg_info: + spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC + layers = _scan_spec_layers(spec_dir) + return { + "mode": "single-repo", + "specLayers": layers, + } + + default_pkg = get_default_package(repo_root) + spec_scope = get_spec_scope(repo_root) + task_pkg = _get_active_task_package(repo_root) + + return { + "mode": "monorepo", + "packages": pkg_info, + "defaultPackage": default_pkg, + "specScope": spec_scope, + "activeTaskPackage": task_pkg, + } diff --git a/.trellis/scripts/common/paths.py b/.trellis/scripts/common/paths.py new file mode 100755 index 0000000..1c5a58e --- /dev/null +++ b/.trellis/scripts/common/paths.py @@ -0,0 +1,447 @@ +#!/usr/bin/env python3 +""" +Common path utilities for Trellis workflow. + +Provides: + get_repo_root - Get repository root directory + get_developer - Get developer name + get_workspace_dir - Get developer workspace directory + get_tasks_dir - Get tasks directory + get_active_journal_file - Get current journal file +""" + +from __future__ import annotations + +import re +from datetime import datetime +from pathlib import Path + + +# ============================================================================= +# Path Constants (change here to rename directories) +# ============================================================================= + +# Directory names +DIR_WORKFLOW = ".trellis" +DIR_WORKSPACE = "workspace" +DIR_TASKS = "tasks" +DIR_ARCHIVE = "archive" +DIR_SPEC = "spec" +DIR_SCRIPTS = "scripts" + +# File names +FILE_DEVELOPER = ".developer" +FILE_CURRENT_TASK = ".current-task" +FILE_TASK_JSON = "task.json" +FILE_JOURNAL_PREFIX = "journal-" + + +# ============================================================================= +# Repository Root +# ============================================================================= + +def get_repo_root(start_path: Path | None = None) -> Path: + """Find the nearest directory containing .trellis/ folder. + + This handles nested git repos correctly (e.g., test project inside another repo). + + Args: + start_path: Starting directory to search from. Defaults to current directory. + + Returns: + Path to repository root, or current directory if no .trellis/ found. + """ + current = (start_path or Path.cwd()).resolve() + + while current != current.parent: + if (current / DIR_WORKFLOW).is_dir(): + return current + current = current.parent + + # Fallback to current directory if no .trellis/ found + return Path.cwd().resolve() + + +# ============================================================================= +# Developer +# ============================================================================= + +def get_developer(repo_root: Path | None = None) -> str | None: + """Get developer name from .developer file. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Developer name or None if not initialized. + """ + if repo_root is None: + repo_root = get_repo_root() + + dev_file = repo_root / DIR_WORKFLOW / FILE_DEVELOPER + + if not dev_file.is_file(): + return None + + try: + content = dev_file.read_text(encoding="utf-8") + for line in content.splitlines(): + if line.startswith("name="): + return line.split("=", 1)[1].strip() + except (OSError, IOError): + pass + + return None + + +def check_developer(repo_root: Path | None = None) -> bool: + """Check if developer is initialized. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + True if developer is initialized. + """ + return get_developer(repo_root) is not None + + +# ============================================================================= +# Tasks Directory +# ============================================================================= + +def get_tasks_dir(repo_root: Path | None = None) -> Path: + """Get tasks directory path. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Path to tasks directory. + """ + if repo_root is None: + repo_root = get_repo_root() + return repo_root / DIR_WORKFLOW / DIR_TASKS + + +# ============================================================================= +# Workspace Directory +# ============================================================================= + +def get_workspace_dir(repo_root: Path | None = None) -> Path | None: + """Get developer workspace directory. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Path to workspace directory or None if developer not set. + """ + if repo_root is None: + repo_root = get_repo_root() + + developer = get_developer(repo_root) + if developer: + return repo_root / DIR_WORKFLOW / DIR_WORKSPACE / developer + return None + + +# ============================================================================= +# Journal File +# ============================================================================= + +def get_active_journal_file(repo_root: Path | None = None) -> Path | None: + """Get the current active journal file. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Path to active journal file or None if not found. + """ + if repo_root is None: + repo_root = get_repo_root() + + workspace_dir = get_workspace_dir(repo_root) + if workspace_dir is None or not workspace_dir.is_dir(): + return None + + latest: Path | None = None + highest = 0 + + for f in workspace_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"): + if not f.is_file(): + continue + + # Extract number from filename + name = f.stem # e.g., "journal-1" + match = re.search(r"(\d+)$", name) + if match: + num = int(match.group(1)) + if num > highest: + highest = num + latest = f + + return latest + + +def count_lines(file_path: Path) -> int: + """Count lines in a file. + + Args: + file_path: Path to file. + + Returns: + Number of lines, or 0 if file doesn't exist. + """ + if not file_path.is_file(): + return 0 + + try: + return len(file_path.read_text(encoding="utf-8").splitlines()) + except (OSError, IOError): + return 0 + + +# ============================================================================= +# Current Task Management +# ============================================================================= + +def normalize_task_ref(task_ref: str) -> str: + """Normalize a task ref for stable runtime storage. + + Stored refs should prefer repo-relative POSIX paths like + `.trellis/tasks/03-27-my-task`, even on Windows. Absolute paths are preserved + unless they can later be converted back to repo-relative form by callers. + """ + normalized = task_ref.strip() + if not normalized: + return "" + + path_obj = Path(normalized) + if path_obj.is_absolute(): + return str(path_obj) + + normalized = normalized.replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + + if normalized.startswith(f"{DIR_TASKS}/"): + return f"{DIR_WORKFLOW}/{normalized}" + + return normalized + + +def resolve_task_ref(task_ref: str, repo_root: Path | None = None) -> Path | None: + """Resolve a task ref to an absolute task directory path.""" + if repo_root is None: + repo_root = get_repo_root() + + normalized = normalize_task_ref(task_ref) + if not normalized: + return None + + path_obj = Path(normalized) + if path_obj.is_absolute(): + return path_obj + + if normalized.startswith(f"{DIR_WORKFLOW}/"): + return repo_root / path_obj + + return repo_root / DIR_WORKFLOW / DIR_TASKS / path_obj + + +def get_current_task( + repo_root: Path | None = None, + platform_input: dict | None = None, + platform: str | None = None, +) -> str | None: + """Get current task directory path (relative to repo_root). + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Relative path to current task directory or None. + """ + if repo_root is None: + repo_root = get_repo_root() + + from .active_task import resolve_active_task + + return resolve_active_task(repo_root, platform_input, platform).task_path + + +def get_current_task_abs( + repo_root: Path | None = None, + platform_input: dict | None = None, + platform: str | None = None, +) -> Path | None: + """Get current task directory absolute path. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Absolute path to current task directory or None. + """ + if repo_root is None: + repo_root = get_repo_root() + + relative = get_current_task(repo_root, platform_input, platform) + if relative: + return resolve_task_ref(relative, repo_root) + return None + + +def get_current_task_source( + repo_root: Path | None = None, + platform_input: dict | None = None, + platform: str | None = None, +) -> tuple[str, str | None, str | None]: + """Get active task source as (`source`, `context_key`, `task_path`).""" + if repo_root is None: + repo_root = get_repo_root() + + from .active_task import get_current_task_source as _get_source + + return _get_source(repo_root, platform_input, platform) + + +def set_current_task( + task_path: str, + repo_root: Path | None = None, + platform_input: dict | None = None, + platform: str | None = None, +) -> bool: + """Set current task in session scope. + + Args: + task_path: Task directory path (relative to repo_root). + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + True on success, False on error. + """ + if repo_root is None: + repo_root = get_repo_root() + + from .active_task import set_active_task + + return set_active_task( + task_path, + repo_root, + platform_input=platform_input, + platform=platform, + ) is not None + + +def clear_current_task( + repo_root: Path | None = None, + platform_input: dict | None = None, + platform: str | None = None, +) -> bool: + """Clear current task in session scope. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + True on success. + """ + if repo_root is None: + repo_root = get_repo_root() + + from .active_task import clear_active_task + + clear_active_task( + repo_root, + platform_input=platform_input, + platform=platform, + ) + return True + + +def has_current_task(repo_root: Path | None = None) -> bool: + """Check if has current task. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + True if current task is set. + """ + return get_current_task(repo_root) is not None + + +# ============================================================================= +# Task ID Generation +# ============================================================================= + +def generate_task_date_prefix() -> str: + """Generate task ID based on date (MM-DD format). + + Returns: + Date prefix string (e.g., "01-21"). + """ + return datetime.now().strftime("%m-%d") + + +# ============================================================================= +# Monorepo / Package Paths +# ============================================================================= + + +def get_spec_dir(package: str | None = None, repo_root: Path | None = None) -> Path: + """Get the spec directory path. + + Single-repo: .trellis/spec + Monorepo with package: .trellis/spec/<package> + + Uses lazy import to avoid circular dependency with config.py. + """ + if repo_root is None: + repo_root = get_repo_root() + + from .config import get_spec_base + + base = get_spec_base(package, repo_root) + return repo_root / DIR_WORKFLOW / base + + +def get_package_path(package: str, repo_root: Path | None = None) -> Path | None: + """Get a package's source directory absolute path from config. + + Returns: + Absolute path to the package directory, or None if not found. + """ + if repo_root is None: + repo_root = get_repo_root() + + from .config import get_packages + + packages = get_packages(repo_root) + if not packages or package not in packages: + return None + + info = packages[package] + if isinstance(info, dict): + rel_path = info.get("path", package) + else: + rel_path = str(info) + + return repo_root / rel_path + + +# ============================================================================= +# Main Entry (for testing) +# ============================================================================= + +if __name__ == "__main__": + repo = get_repo_root() + print(f"Repository root: {repo}") + print(f"Developer: {get_developer(repo)}") + print(f"Tasks dir: {get_tasks_dir(repo)}") + print(f"Workspace dir: {get_workspace_dir(repo)}") + print(f"Journal file: {get_active_journal_file(repo)}") + print(f"Current task: {get_current_task(repo)}") diff --git a/.trellis/scripts/common/safe_commit.py b/.trellis/scripts/common/safe_commit.py new file mode 100755 index 0000000..1a1cba2 --- /dev/null +++ b/.trellis/scripts/common/safe_commit.py @@ -0,0 +1,315 @@ +""" +Safe git-add helpers for Trellis-owned paths. + +Why this module exists +---------------------- +A real user incident: a project's `.gitignore` listed `.trellis/` (company-wide +template / personal habit). When `add_session.py` and `task.py archive` ran +their auto-commit and `git add` failed with `ignored by .gitignore`, the AI +agent driving the workflow "fixed" it by retrying with +`git add -f .trellis/` — which fan-out-included every ignored subtree +(`.trellis/.backup-*/`, `.trellis/worktrees/`, `.trellis/.template-hashes.json`, +`.trellis/.runtime/`), committing 548 files / 83474 lines of caches/backups. + +Design +------ +- Scripts only stage SPECIFIC product paths (journal files, index.md, the + current task dir, the archive dir). Never the whole `.trellis/` tree. +- If plain `git add <specific>` fails with "ignored by", DO NOT retry with + ``-f``. The presence of `.trellis/` in `.gitignore` is treated as user + intent ("keep .trellis/ local-only"). The script warns and skips the + auto-commit; users who want auto-staging can either fix their `.gitignore` + or set ``session_auto_commit: false`` and manage git themselves. +- The warning includes a negative example: ``Do NOT use `git add -f .trellis/` ...`` + so any AI rereading the log doesn't reinvent the bug. + +History note: 0.5.10 introduced an automatic ``git add -f`` retry on the +specific paths. That was reverted in 0.5.11 — auto-forcing into a tree the +user had gitignored violates user intent even when the path list is narrow. +The wider-grain forbidden command stays forbidden, and the narrow-grain auto +``-f`` is gone too. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from .git import run_git +from .paths import ( + DIR_ARCHIVE, + DIR_TASKS, + DIR_WORKFLOW, + DIR_WORKSPACE, + FILE_JOURNAL_PREFIX, + get_developer, +) + + +# Paths under .trellis/ that must NEVER be auto-staged. Listed here so the +# warning to the user can show concrete subpaths to ignore individually +# instead of ignoring the whole `.trellis/` tree. +TRELLIS_IGNORED_SUBPATHS = ( + ".trellis/.backup-*", + ".trellis/worktrees/", + ".trellis/.template-hashes.json", + ".trellis/.runtime/", + ".trellis/.cache/", +) + + +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 + arguments to git. The caller is responsible for `git diff --cached` + checking afterwards. + + Included: + - .trellis/workspace/<developer>/journal-*.md + - .trellis/workspace/<developer>/index.md + - .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] = [] + + # Workspace journal files + index.md + developer = get_developer(repo_root) + if developer: + ws = repo_root / DIR_WORKFLOW / DIR_WORKSPACE / developer + if ws.is_dir(): + for f in sorted(ws.glob(f"{FILE_JOURNAL_PREFIX}*.md")): + if f.is_file(): + paths.append( + f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{f.name}" + ) + index_md = ws / "index.md" + if index_md.is_file(): + paths.append( + f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/index.md" + ) + + tasks_dir = repo_root / DIR_WORKFLOW / DIR_TASKS + if not tasks_dir.is_dir(): + return paths + + 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 + + +def safe_archive_paths_to_add( + repo_root: Path, + task_name: str | None = None, + modified_children: list[str] | None = None, +) -> list[str]: + """Return paths to stage after `task.py archive`. + + Scoped to ONLY the paths the archive operation actually touched: + + - the archive subtree (where the freshly-moved task lives) + - the source task directory (for source-side deletes; caller pairs + this with `git rm --cached` since `git add` won't stage deletes + for a path that no longer exists in the working tree) + - any child task directories whose `task.json` was edited to drop + the archived parent (parent-children relationship update) + + This narrow scope avoids "scope creep" — dirty changes in OTHER + active task dirs (parallel-window edits) are NOT bundled into the + archive commit. Callers handle each kind of change in its own + commit boundary. + + Backwards-compat: with no arguments, the function walks the whole + `.trellis/tasks/` subtree the old way (active tasks + archive). New + callers should always pass `task_name`. + """ + paths: list[str] = [] + tasks_dir = repo_root / DIR_WORKFLOW / DIR_TASKS + if not tasks_dir.is_dir(): + return paths + + archive_dir = tasks_dir / DIR_ARCHIVE + + if task_name is not None: + # Narrow scope — only paths that still exist on disk (so + # `git add` doesn't choke on the moved-away source). The caller + # handles the source-side deletes via `git rm --cached` + # explicitly. + if archive_dir.is_dir(): + paths.append( + f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}" + ) + for child_name in modified_children or []: + paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child_name}") + return paths + + # Legacy wide scope (no task_name): preserve old behavior so callers + # that have not been updated keep working. + if archive_dir.is_dir(): + paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}") + 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}") + return paths + + +def _stderr_indicates_ignored(stderr: str) -> bool: + """git add error indicates the path is excluded by .gitignore.""" + if not stderr: + return False + lowered = stderr.lower() + return "ignored by" in lowered + + +def safe_git_add( + paths: list[str], repo_root: Path +) -> tuple[bool, bool, str]: + """Run `git add` on specific paths; never retry with -f. + + Returns ``(success, used_force, stderr)``. The ``used_force`` field is + kept for signature compatibility with the 0.5.10 implementation but is + always ``False`` — we never auto-force. + + Behavior: + - No paths passed → success, no force, empty stderr. + - Plain ``git add -- <paths>`` succeeds → return success. + - Plain fails (any reason — ignored or otherwise) → return failure with + the stderr. Callers should inspect the stderr (see + :func:`print_gitignore_warning`) and skip the auto-commit. + """ + if not paths: + return True, False, "" + + rc, _, err = run_git(["add", "--", *paths], cwd=repo_root) + if rc == 0: + return True, False, "" + return False, False, err + + +def print_gitignore_warning(paths: list[str]) -> None: + """Explain to the user (and any AI reading the log) what to do. + + CRITICAL: includes the negative example + ``Do NOT use `git add -f .trellis/``` — agents reading the warning are + known to invent that command, which fans out to ignored caches/backups. + """ + print( + "[WARN] git add failed because .trellis/ paths are ignored by your .gitignore.", + file=sys.stderr, + ) + print( + "[WARN] Skipping auto-commit. The journal/task files were still written to disk;", + file=sys.stderr, + ) + print( + "[WARN] git was not touched.", + file=sys.stderr, + ) + print("[WARN]", file=sys.stderr) + print( + "[WARN] Trellis manages these specific paths and they should be tracked:", + file=sys.stderr, + ) + if paths: + for p in paths: + print(f"[WARN] {p}", file=sys.stderr) + else: + print( + "[WARN] .trellis/workspace/<developer>/{journal-*.md,index.md}", + file=sys.stderr, + ) + print( + "[WARN] .trellis/tasks/<task-dir>/", + file=sys.stderr, + ) + print( + "[WARN] .trellis/tasks/archive/", + file=sys.stderr, + ) + print("[WARN]", file=sys.stderr) + print( + "[WARN] Recommended: change your .gitignore from `.trellis/` to specific", + file=sys.stderr, + ) + print( + "[WARN] subpaths that should remain ignored, e.g.:", + file=sys.stderr, + ) + for sub in TRELLIS_IGNORED_SUBPATHS: + print(f"[WARN] {sub}", file=sys.stderr) + print("[WARN]", file=sys.stderr) + print( + "[WARN] Or, if you intentionally keep .trellis/ local-only, set in", + file=sys.stderr, + ) + print( + "[WARN] .trellis/config.yaml:", + file=sys.stderr, + ) + print( + "[WARN] session_auto_commit: false", + file=sys.stderr, + ) + print( + "[WARN] so the scripts skip git entirely and you can review / commit", + file=sys.stderr, + ) + print( + "[WARN] manually with `git status` / `git add` / `git commit`.", + file=sys.stderr, + ) + print("[WARN]", file=sys.stderr) + print( + "[WARN] Do NOT use `git add -f .trellis/` — it pulls in backups, worktrees,", + file=sys.stderr, + ) + print( + "[WARN] and runtime caches that should never be committed.", + file=sys.stderr, + ) diff --git a/.trellis/scripts/common/session_context.py b/.trellis/scripts/common/session_context.py new file mode 100755 index 0000000..d30a519 --- /dev/null +++ b/.trellis/scripts/common/session_context.py @@ -0,0 +1,821 @@ +#!/usr/bin/env python3 +""" +Session context generation (default + record modes). + +Provides: + get_context_json - JSON output for default mode + get_context_text - Text output for default mode + get_context_record_json - JSON for record mode + get_context_text_record - Text for record mode + output_json - Print JSON + output_text - Print text +""" + +from __future__ import annotations + +import json +import os +import re +import subprocess +from pathlib import Path + +from .active_task import resolve_context_key +from .config import get_git_packages +from .git import run_git +from .packages_context import get_packages_section +from .tasks import iter_active_tasks, load_task, get_all_statuses, children_progress +from .paths import ( + DIR_SCRIPTS, + DIR_SPEC, + DIR_TASKS, + DIR_WORKFLOW, + DIR_WORKSPACE, + count_lines, + get_active_journal_file, + get_current_task, + get_current_task_source, + get_developer, + get_repo_root, + get_tasks_dir, +) + + +# ============================================================================= +# Helpers +# ============================================================================= + +_PACKAGE_NAME = "@mindfoldhq/trellis" +_UPDATE_CHECK_TIMEOUT_SECONDS = 1.0 +_VERSION_RE = re.compile( + r"^\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?\s*$" +) +_VERSION_TOKEN_RE = re.compile(r"\b\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?\b") +_POLYREPO_IGNORED_DIRS = { + "node_modules", + "target", + "dist", + "build", + "out", + "bin", + "obj", + "vendor", + "coverage", + "tmp", + "__pycache__", +} +_POLYREPO_SCAN_MAX_DEPTH = 2 + + +def _is_git_worktree(path: Path) -> bool: + """Return True when path is inside a Git worktree.""" + rc, out, _ = run_git(["rev-parse", "--is-inside-work-tree"], cwd=path) + return rc == 0 and out.strip().lower() == "true" + + +def _parse_recent_commits(log_output: str) -> list[dict]: + """Parse `git log --oneline` output into structured commit entries.""" + commits = [] + for line in log_output.splitlines(): + if not line.strip(): + continue + parts = line.split(" ", 1) + if len(parts) >= 2: + commits.append({"hash": parts[0], "message": parts[1]}) + elif len(parts) == 1: + commits.append({"hash": parts[0], "message": ""}) + return commits + + +def _collect_git_repo_info(name: str, rel_path: str, repo_dir: Path) -> dict | None: + """Collect Git status for one known repository directory.""" + if not (repo_dir / ".git").exists(): + return None + + _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_dir) + branch = branch_out.strip() or "unknown" + + _, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_dir) + changes = len([l for l in status_out.splitlines() if l.strip()]) + + _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_dir) + + return { + "name": name, + "path": rel_path, + "branch": branch, + "isClean": changes == 0, + "uncommittedChanges": changes, + "recentCommits": _parse_recent_commits(log_out), + } + + +def _collect_root_git_info(repo_root: Path) -> dict: + """Collect root Git info without pretending a non-Git root is clean.""" + if not _is_git_worktree(repo_root): + return { + "isRepo": False, + "branch": "", + "isClean": False, + "uncommittedChanges": 0, + "recentCommits": [], + } + + _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root) + branch = branch_out.strip() or "unknown" + + _, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root) + status_lines = [line for line in status_out.splitlines() if line.strip()] + + _, short_out, _ = run_git(["status", "--short"], cwd=repo_root) + + _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root) + + return { + "isRepo": True, + "branch": branch, + "isClean": len(status_lines) == 0, + "uncommittedChanges": len(status_lines), + "statusShort": short_out.splitlines(), + "recentCommits": _parse_recent_commits(log_out), + } + + +def _discover_child_git_repos(repo_root: Path) -> list[tuple[str, str]]: + """Discover child Git repositories using the init-time polyrepo heuristic.""" + found: list[str] = [] + + def is_candidate_dir(path: Path) -> bool: + name = path.name + return not name.startswith(".") and name not in _POLYREPO_IGNORED_DIRS + + def scan(rel_dir: Path, depth: int) -> None: + if depth >= _POLYREPO_SCAN_MAX_DEPTH: + return + abs_dir = repo_root / rel_dir + try: + children = sorted(abs_dir.iterdir(), key=lambda p: p.name) + except OSError: + return + + for child in children: + if not child.is_dir() or not is_candidate_dir(child): + continue + + child_rel = ( + rel_dir / child.name if rel_dir != Path(".") else Path(child.name) + ) + if (child / ".git").exists(): + found.append(child_rel.as_posix()) + continue + scan(child_rel, depth + 1) + + scan(Path("."), 0) + if len(found) < 2: + return [] + return [(path.replace("/", "_"), path) for path in sorted(found)] + + +def _collect_package_git_info( + repo_root: Path, + discover_unconfigured: bool = False, +) -> list[dict]: + """Collect Git status for independent package repositories. + + Packages marked with ``git: true`` in config.yaml are authoritative. + When the Trellis root is not a Git repo and no configured package repos are + available, optionally fall back to the bounded polyrepo child scan. + + Returns: + List of dicts with keys: name, path, branch, isClean, + uncommittedChanges, recentCommits. + Empty list if no git-repo packages are configured. + """ + git_pkgs = get_git_packages(repo_root) + result = [] + for pkg_name, pkg_path in git_pkgs.items(): + pkg_dir = repo_root / pkg_path + info = _collect_git_repo_info(pkg_name, pkg_path, pkg_dir) + if info is not None: + result.append(info) + + if result or not discover_unconfigured: + return result + + discovered = [] + for pkg_name, pkg_path in _discover_child_git_repos(repo_root): + info = _collect_git_repo_info(pkg_name, pkg_path, repo_root / pkg_path) + if info is not None: + discovered.append(info) + return discovered + + +def _append_root_git_context(lines: list[str], root_git_info: dict) -> None: + """Append root Git status without misleading non-Git roots.""" + lines.append("## GIT STATUS") + if not root_git_info["isRepo"]: + lines.append("Root is not a Git repository.") + lines.append("Run Git commands from the package repository paths listed below.") + else: + lines.append(f"Branch: {root_git_info['branch']}") + if root_git_info["isClean"]: + lines.append("Working directory: Clean") + else: + lines.append( + f"Working directory: {root_git_info['uncommittedChanges']} " + "uncommitted change(s)" + ) + lines.append("") + lines.append("Changes:") + for line in root_git_info.get("statusShort", [])[:10]: + lines.append(line) + lines.append("") + + lines.append("## RECENT COMMITS") + if not root_git_info["isRepo"]: + lines.append( + "Root has no Git commit history because it is not a Git repository." + ) + elif root_git_info["recentCommits"]: + for commit in root_git_info["recentCommits"]: + lines.append(f"{commit['hash']} {commit['message']}") + else: + lines.append("(no commits)") + lines.append("") + + +def _append_package_git_context(lines: list[str], package_git_info: list[dict]) -> None: + """Append Git status and recent commits for package repositories.""" + for pkg in package_git_info: + lines.append(f"## GIT STATUS ({pkg['name']}: {pkg['path']})") + lines.append(f"Branch: {pkg['branch']}") + if pkg["isClean"]: + lines.append("Working directory: Clean") + else: + lines.append( + f"Working directory: {pkg['uncommittedChanges']} uncommitted change(s)" + ) + lines.append("") + lines.append(f"## RECENT COMMITS ({pkg['name']}: {pkg['path']})") + if pkg["recentCommits"]: + for commit in pkg["recentCommits"]: + lines.append(f"{commit['hash']} {commit['message']}") + else: + lines.append("(no commits)") + lines.append("") + + +def _read_project_version(repo_root: Path) -> str | None: + try: + version = (repo_root / DIR_WORKFLOW / ".version").read_text( + encoding="utf-8" + ).strip() + except OSError: + return None + return version or None + + +def _fetch_trellis_version_output() -> str | None: + try: + result = subprocess.run( + ["trellis", "--version"], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=_UPDATE_CHECK_TIMEOUT_SECONDS, + ) + except (OSError, subprocess.SubprocessError, TimeoutError): + return None + + if result.returncode != 0: + return None + output = f"{result.stdout}\n{result.stderr}".strip() + return output or None + + +def _extract_available_update_version(output: str) -> str | None: + update_match = re.search( + r"Trellis update available:\s*" + r"(?P<current>\S+)\s*(?:→|->)\s*(?P<latest>\S+)", + output, + ) + if update_match: + return update_match.group("latest").strip() + candidates = _VERSION_TOKEN_RE.findall(output) + return candidates[-1] if candidates else None + + +def _resolve_available_update_version() -> str | None: + output = _fetch_trellis_version_output() + if not output: + return None + return _extract_available_update_version(output) + + +def _parse_version(version: str) -> tuple[tuple[int, int, int], tuple[str, ...] | None] | None: + match = _VERSION_RE.match(version) + if not match: + return None + major, minor, patch, prerelease = match.groups() + numbers = (int(major), int(minor or "0"), int(patch or "0")) + prerelease_parts = tuple(prerelease.split(".")) if prerelease else None + return numbers, prerelease_parts + + +def _compare_prerelease( + left: tuple[str, ...] | None, + right: tuple[str, ...] | None, +) -> int: + if left is None and right is None: + return 0 + if left is None: + return 1 + if right is None: + return -1 + + for left_part, right_part in zip(left, right): + if left_part == right_part: + continue + left_numeric = left_part.isdigit() + right_numeric = right_part.isdigit() + if left_numeric and right_numeric: + left_int = int(left_part) + right_int = int(right_part) + return (left_int > right_int) - (left_int < right_int) + if left_numeric: + return -1 + if right_numeric: + return 1 + return (left_part > right_part) - (left_part < right_part) + + return (len(left) > len(right)) - (len(left) < len(right)) + + +def _compare_versions(left: str, right: str) -> int | None: + parsed_left = _parse_version(left) + parsed_right = _parse_version(right) + if parsed_left is None or parsed_right is None: + return None + + left_numbers, left_prerelease = parsed_left + right_numbers, right_prerelease = parsed_right + if left_numbers != right_numbers: + return (left_numbers > right_numbers) - (left_numbers < right_numbers) + return _compare_prerelease(left_prerelease, right_prerelease) + + +def _update_marker_path(repo_root: Path) -> Path: + context_key = resolve_context_key() + if not context_key: + terminal_key = os.environ.get("TERM_SESSION_ID", "").strip() + context_key = terminal_key or f"ppid-{os.getppid()}" + safe_key = re.sub(r"[^A-Za-z0-9._-]+", "_", context_key).strip("._-") + if not safe_key: + safe_key = "session" + return ( + repo_root + / DIR_WORKFLOW + / ".runtime" + / f"update-check-{safe_key[:160]}.marker" + ) + + +def _mark_update_check_attempted(repo_root: Path) -> bool: + marker_path = _update_marker_path(repo_root) + if marker_path.exists(): + return False + try: + marker_path.parent.mkdir(parents=True, exist_ok=True) + marker_path.write_text("checked\n", encoding="utf-8") + except OSError: + pass + return True + + +def _get_update_hint(repo_root: Path) -> str | None: + marker_path = _update_marker_path(repo_root) + if marker_path.exists(): + return None + + current_version = _read_project_version(repo_root) + if not current_version: + return None + + latest_version = _resolve_available_update_version() + if not latest_version: + return None + + _mark_update_check_attempted(repo_root) + comparison = _compare_versions(current_version, latest_version) + if comparison is None or comparison >= 0: + return None + + return ( + f"Trellis update available: {current_version} -> {latest_version}, " + "run trellis upgrade" + ) + + +# ============================================================================= +# JSON Output +# ============================================================================= + +def get_context_json(repo_root: Path | None = None) -> dict: + """Get context as a dictionary. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Context dictionary. + """ + if repo_root is None: + repo_root = get_repo_root() + + developer = get_developer(repo_root) + tasks_dir = get_tasks_dir(repo_root) + journal_file = get_active_journal_file(repo_root) + + journal_lines = 0 + journal_relative = "" + if journal_file and developer: + journal_lines = count_lines(journal_file) + journal_relative = ( + f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}" + ) + + root_git_info = _collect_root_git_info(repo_root) + + # Tasks + tasks = [ + { + "dir": t.dir_name, + "name": t.name, + "status": t.status, + "children": list(t.children), + "parent": t.parent, + } + for t in iter_active_tasks(tasks_dir) + ] + + # Package git repos (independent sub-repositories) + pkg_git_info = _collect_package_git_info( + repo_root, + discover_unconfigured=not root_git_info["isRepo"], + ) + + result = { + "developer": developer or "", + "git": { + "isRepo": root_git_info["isRepo"], + "branch": root_git_info["branch"], + "isClean": root_git_info["isClean"], + "uncommittedChanges": root_git_info["uncommittedChanges"], + "recentCommits": root_git_info["recentCommits"], + }, + "tasks": { + "active": tasks, + "directory": f"{DIR_WORKFLOW}/{DIR_TASKS}", + }, + "journal": { + "file": journal_relative, + "lines": journal_lines, + "nearLimit": journal_lines > 1800, + }, + } + + if pkg_git_info: + result["packageGit"] = pkg_git_info + + return result + + +def output_json(repo_root: Path | None = None) -> None: + """Output context in JSON format. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + """ + context = get_context_json(repo_root) + print(json.dumps(context, indent=2, ensure_ascii=False)) + + +# ============================================================================= +# Text Output +# ============================================================================= + +def get_context_text(repo_root: Path | None = None) -> str: + """Get context as formatted text. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Formatted text output. + """ + if repo_root is None: + repo_root = get_repo_root() + + lines = [] + lines.append("========================================") + lines.append("SESSION CONTEXT") + lines.append("========================================") + lines.append("") + + developer = get_developer(repo_root) + + # Developer section + lines.append("## DEVELOPER") + if not developer: + lines.append( + f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>" + ) + return "\n".join(lines) + + lines.append(f"Name: {developer}") + lines.append("") + + root_git_info = _collect_root_git_info(repo_root) + _append_root_git_context(lines, root_git_info) + + # Package git repos — independent sub-repositories + _append_package_git_context( + lines, + _collect_package_git_info( + repo_root, + discover_unconfigured=not root_git_info["isRepo"], + ), + ) + + # Current task + lines.append("## CURRENT TASK") + current_task = get_current_task(repo_root) + if current_task: + current_task_dir = repo_root / current_task + source_type, context_key, _ = get_current_task_source(repo_root) + lines.append(f"Path: {current_task}") + lines.append( + f"Source: {source_type}" + (f":{context_key}" if context_key else "") + ) + + ct = load_task(current_task_dir) + if ct: + lines.append(f"Name: {ct.name}") + lines.append(f"Status: {ct.status}") + lines.append(f"Created: {ct.raw.get('createdAt', 'unknown')}") + if ct.description: + lines.append(f"Description: {ct.description}") + + # Check for prd.md + prd_file = current_task_dir / "prd.md" + if prd_file.is_file(): + lines.append("") + lines.append("[!] This task has prd.md - read it for task details") + else: + lines.append("(none)") + lines.append("") + + # Active tasks + lines.append("## ACTIVE TASKS") + tasks_dir = get_tasks_dir(repo_root) + task_count = 0 + + # Collect all task data for hierarchy display + all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)} + all_statuses = {name: t.status for name, t in all_tasks.items()} + + def _print_task_tree(name: str, indent: int = 0) -> None: + nonlocal task_count + t = all_tasks[name] + progress = children_progress(t.children, all_statuses) + prefix = " " * indent + lines.append(f"{prefix}- {name}/ ({t.status}){progress} @{t.assignee or '-'}") + task_count += 1 + for child in t.children: + if child in all_tasks: + _print_task_tree(child, indent + 1) + + for dir_name in sorted(all_tasks.keys()): + if not all_tasks[dir_name].parent: + _print_task_tree(dir_name) + + if task_count == 0: + lines.append("(no active tasks)") + lines.append(f"Total: {task_count} active task(s)") + lines.append("") + + # My tasks + lines.append("## MY TASKS (Assigned to me)") + my_task_count = 0 + + for t in all_tasks.values(): + if t.assignee == developer and t.status != "done": + progress = children_progress(t.children, all_statuses) + lines.append(f"- [{t.priority}] {t.title} ({t.status}){progress}") + my_task_count += 1 + + if my_task_count == 0: + lines.append("(no tasks assigned to you)") + lines.append("") + + # Journal file + lines.append("## JOURNAL FILE") + journal_file = get_active_journal_file(repo_root) + if journal_file: + journal_lines = count_lines(journal_file) + relative = f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}" + lines.append(f"Active file: {relative}") + lines.append(f"Line count: {journal_lines} / 2000") + if journal_lines > 1800: + lines.append("[!] WARNING: Approaching 2000 line limit!") + else: + lines.append("No journal file found") + lines.append("") + + # Packages + packages_text = get_packages_section(repo_root) + if packages_text: + lines.append(packages_text) + lines.append("") + + # Paths + lines.append("## PATHS") + lines.append(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/") + lines.append(f"Tasks: {DIR_WORKFLOW}/{DIR_TASKS}/") + lines.append(f"Spec: {DIR_WORKFLOW}/{DIR_SPEC}/") + lines.append("") + + lines.append("========================================") + + return "\n".join(lines) + + +# ============================================================================= +# Record Mode +# ============================================================================= + +def get_context_record_json(repo_root: Path | None = None) -> dict: + """Get record-mode context as a dictionary. + + Focused on: my active tasks, git status, current task. + """ + if repo_root is None: + repo_root = get_repo_root() + + developer = get_developer(repo_root) + tasks_dir = get_tasks_dir(repo_root) + + root_git_info = _collect_root_git_info(repo_root) + + # My tasks (single pass — collect statuses and filter by assignee) + all_tasks_list = list(iter_active_tasks(tasks_dir)) + all_statuses = {t.dir_name: t.status for t in all_tasks_list} + + my_tasks = [] + for t in all_tasks_list: + if t.assignee == developer: + done = sum( + 1 for c in t.children + if all_statuses.get(c) in ("completed", "done") + ) + my_tasks.append({ + "dir": t.dir_name, + "title": t.title, + "status": t.status, + "priority": t.priority, + "children": list(t.children), + "childrenDone": done, + "parent": t.parent, + "meta": t.meta, + }) + + # Current task + current_task_info = None + current_task = get_current_task(repo_root) + if current_task: + source_type, context_key, _ = get_current_task_source(repo_root) + ct = load_task(repo_root / current_task) + if ct: + current_task_info = { + "path": current_task, + "name": ct.name, + "status": ct.status, + "source": source_type, + "contextKey": context_key, + } + + # Package git repos + pkg_git_info = _collect_package_git_info( + repo_root, + discover_unconfigured=not root_git_info["isRepo"], + ) + + result = { + "developer": developer or "", + "git": { + "isRepo": root_git_info["isRepo"], + "branch": root_git_info["branch"], + "isClean": root_git_info["isClean"], + "uncommittedChanges": root_git_info["uncommittedChanges"], + "recentCommits": root_git_info["recentCommits"], + }, + "myTasks": my_tasks, + "currentTask": current_task_info, + } + + if pkg_git_info: + result["packageGit"] = pkg_git_info + + return result + + +def get_context_text_record(repo_root: Path | None = None) -> str: + """Get context as formatted text for record-session mode. + + Focused output: MY ACTIVE TASKS first (with [!!!] emphasis), + then GIT STATUS, RECENT COMMITS, CURRENT TASK. + """ + if repo_root is None: + repo_root = get_repo_root() + + lines: list[str] = [] + lines.append("========================================") + lines.append("SESSION CONTEXT (RECORD MODE)") + lines.append("========================================") + lines.append("") + + developer = get_developer(repo_root) + if not developer: + lines.append( + f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>" + ) + return "\n".join(lines) + + # MY ACTIVE TASKS — first and prominent + lines.append(f"## [!!!] MY ACTIVE TASKS (Assigned to {developer})") + lines.append("[!] Review whether any should be archived before recording this session.") + lines.append("") + + tasks_dir = get_tasks_dir(repo_root) + my_task_count = 0 + + # Single pass — collect all tasks and filter by assignee + all_statuses = get_all_statuses(tasks_dir) + + for t in iter_active_tasks(tasks_dir): + if t.assignee == developer: + progress = children_progress(t.children, all_statuses) + lines.append(f"- [{t.priority}] {t.title} ({t.status}){progress} — {t.dir_name}") + my_task_count += 1 + + if my_task_count == 0: + lines.append("(no active tasks assigned to you)") + lines.append("") + + root_git_info = _collect_root_git_info(repo_root) + _append_root_git_context(lines, root_git_info) + + # Package git repos — independent sub-repositories + _append_package_git_context( + lines, + _collect_package_git_info( + repo_root, + discover_unconfigured=not root_git_info["isRepo"], + ), + ) + + # CURRENT TASK + lines.append("## CURRENT TASK") + current_task = get_current_task(repo_root) + if current_task: + source_type, context_key, _ = get_current_task_source(repo_root) + lines.append(f"Path: {current_task}") + lines.append( + f"Source: {source_type}" + (f":{context_key}" if context_key else "") + ) + ct = load_task(repo_root / current_task) + if ct: + lines.append(f"Name: {ct.name}") + lines.append(f"Status: {ct.status}") + else: + lines.append("(none)") + lines.append("") + + lines.append("========================================") + + return "\n".join(lines) + + +def output_text(repo_root: Path | None = None) -> None: + """Output context in text format. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + """ + if repo_root is None: + repo_root = get_repo_root() + update_hint = _get_update_hint(repo_root) + if update_hint: + print(update_hint) + print("") + print(get_context_text(repo_root)) diff --git a/.trellis/scripts/common/task_context.py b/.trellis/scripts/common/task_context.py new file mode 100755 index 0000000..7ffc9f5 --- /dev/null +++ b/.trellis/scripts/common/task_context.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +""" +Task JSONL context management. + +Provides: + cmd_add_context - Add entry to JSONL context file + cmd_validate - Validate JSONL context files + cmd_list_context - List JSONL context entries + +Note: + ``cmd_init_context`` was removed in v0.5.0-beta.12. JSONL context files + are now seeded at ``task.py create`` time with a self-describing + ``_example`` line; the AI agent curates real entries during planning when + the task needs sub-agent/spec context. See ``.trellis/workflow.md`` for the + current planning artifact contract. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from .log import Colors, colored +from .paths import get_repo_root +from .task_utils import resolve_task_dir + + +# ============================================================================= +# Command: add-context +# ============================================================================= + +def cmd_add_context(args: argparse.Namespace) -> int: + """Add entry to JSONL context file.""" + repo_root = get_repo_root() + target_dir = resolve_task_dir(args.dir, repo_root) + + jsonl_name = args.file + path = args.path + reason = args.reason or "Added manually" + + if not target_dir.is_dir(): + print(colored(f"Error: Directory not found: {target_dir}", Colors.RED)) + return 1 + + # Support shorthand + if not jsonl_name.endswith(".jsonl"): + jsonl_name = f"{jsonl_name}.jsonl" + + jsonl_file = target_dir / jsonl_name + full_path = repo_root / path + + entry_type = "file" + if full_path.is_dir(): + entry_type = "directory" + if not path.endswith("/"): + path = f"{path}/" + elif not full_path.is_file(): + print(colored(f"Error: Path not found: {path}", Colors.RED)) + return 1 + + # Check if already exists + if jsonl_file.is_file(): + content = jsonl_file.read_text(encoding="utf-8") + if f'"{path}"' in content: + print(colored(f"Warning: Entry already exists for {path}", Colors.YELLOW)) + return 0 + + # Add entry + entry: dict + if entry_type == "directory": + entry = {"file": path, "type": "directory", "reason": reason} + else: + entry = {"file": path, "reason": reason} + + with jsonl_file.open("a", encoding="utf-8") as f: + f.write(json.dumps(entry, ensure_ascii=False) + "\n") + + print(colored(f"Added {entry_type}: {path}", Colors.GREEN)) + return 0 + + +# ============================================================================= +# Command: validate +# ============================================================================= + +def cmd_validate(args: argparse.Namespace) -> int: + """Validate JSONL context files.""" + repo_root = get_repo_root() + target_dir = resolve_task_dir(args.dir, repo_root) + + if not target_dir.is_dir(): + print(colored("Error: task directory required", Colors.RED)) + return 1 + + print(colored("=== Validating Context Files ===", Colors.BLUE)) + print(f"Target dir: {target_dir}") + print() + + total_errors = 0 + for jsonl_name in ["implement.jsonl", "check.jsonl"]: + jsonl_file = target_dir / jsonl_name + errors = _validate_jsonl(jsonl_file, repo_root) + total_errors += errors + + print() + if total_errors == 0: + print(colored("✓ All validations passed", Colors.GREEN)) + return 0 + else: + print(colored(f"✗ Validation failed ({total_errors} errors)", Colors.RED)) + return 1 + + +def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int: + """Validate a single JSONL file. + + Seed rows (no ``file`` field — typically ``{"_example": "..."}``) are + skipped silently; they are self-describing comments, not real entries. + """ + file_name = jsonl_file.name + errors = 0 + + if not jsonl_file.is_file(): + print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}") + return 0 + + line_num = 0 + real_entries = 0 + for line in jsonl_file.read_text(encoding="utf-8").splitlines(): + line_num += 1 + if not line.strip(): + continue + + try: + data = json.loads(line) + except json.JSONDecodeError: + print(f" {colored(f'{file_name}:{line_num}: Invalid JSON', Colors.RED)}") + errors += 1 + continue + + file_path = data.get("file") + entry_type = data.get("type", "file") + + if not file_path: + # Seed / comment row — skip silently + continue + + real_entries += 1 + full_path = repo_root / file_path + if entry_type == "directory": + if not full_path.is_dir(): + print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}") + errors += 1 + else: + if not full_path.is_file(): + print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}") + errors += 1 + + if errors == 0: + print(f" {colored(f'{file_name}: ✓ ({real_entries} entries)', Colors.GREEN)}") + else: + print(f" {colored(f'{file_name}: ✗ ({errors} errors)', Colors.RED)}") + + return errors + + +# ============================================================================= +# Command: list-context +# ============================================================================= + +def cmd_list_context(args: argparse.Namespace) -> int: + """List JSONL context entries.""" + repo_root = get_repo_root() + target_dir = resolve_task_dir(args.dir, repo_root) + + if not target_dir.is_dir(): + print(colored("Error: task directory required", Colors.RED)) + return 1 + + print(colored("=== Context Files ===", Colors.BLUE)) + print() + + for jsonl_name in ["implement.jsonl", "check.jsonl"]: + jsonl_file = target_dir / jsonl_name + if not jsonl_file.is_file(): + continue + + print(colored(f"[{jsonl_name}]", Colors.CYAN)) + + count = 0 + seed_only = True + for line in jsonl_file.read_text(encoding="utf-8").splitlines(): + if not line.strip(): + continue + + try: + data = json.loads(line) + except json.JSONDecodeError: + continue + + file_path = data.get("file") + if not file_path: + # Seed / comment row — don't count as a real entry + continue + seed_only = False + + count += 1 + entry_type = data.get("type", "file") + reason = data.get("reason", "-") + + if entry_type == "directory": + print(f" {colored(f'{count}.', Colors.GREEN)} [DIR] {file_path}") + else: + print(f" {colored(f'{count}.', Colors.GREEN)} {file_path}") + print(f" {colored('→', Colors.YELLOW)} {reason}") + + if seed_only: + print(f" {colored('(no curated entries yet — only seed row)', Colors.YELLOW)}") + + print() + + return 0 diff --git a/.trellis/scripts/common/task_queue.py b/.trellis/scripts/common/task_queue.py new file mode 100755 index 0000000..f7485e2 --- /dev/null +++ b/.trellis/scripts/common/task_queue.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +""" +Task queue utility functions. + +Provides: + list_tasks_by_status - List tasks by status + list_pending_tasks - List tasks with pending status + list_tasks_by_assignee - List tasks by assignee + list_my_tasks - List tasks assigned to current developer + get_task_stats - Get P0/P1/P2/P3 counts +""" + +from __future__ import annotations + +from pathlib import Path + +from .paths import ( + get_repo_root, + get_developer, + get_tasks_dir, +) +from .tasks import iter_active_tasks + + +# ============================================================================= +# Internal helper +# ============================================================================= + +def _task_to_dict(t) -> dict: + """Convert TaskInfo to the dict format callers expect.""" + return { + "priority": t.priority, + "id": t.raw.get("id", ""), + "title": t.title, + "status": t.status, + "assignee": t.assignee or "-", + "dir": t.dir_name, + "children": list(t.children), + "parent": t.parent, + } + + +# ============================================================================= +# Public Functions +# ============================================================================= + +def list_tasks_by_status( + filter_status: str | None = None, + repo_root: Path | None = None +) -> list[dict]: + """List tasks by status. + + Args: + filter_status: Optional status filter. + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + List of task info dicts with keys: priority, id, title, status, assignee. + """ + if repo_root is None: + repo_root = get_repo_root() + + tasks_dir = get_tasks_dir(repo_root) + results = [] + + for t in iter_active_tasks(tasks_dir): + if filter_status and t.status != filter_status: + continue + results.append(_task_to_dict(t)) + + return results + + +def list_pending_tasks(repo_root: Path | None = None) -> list[dict]: + """List pending tasks. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + List of task info dicts. + """ + return list_tasks_by_status("planning", repo_root) + + +def list_tasks_by_assignee( + assignee: str, + filter_status: str | None = None, + repo_root: Path | None = None +) -> list[dict]: + """List tasks assigned to a specific developer. + + Args: + assignee: Developer name. + filter_status: Optional status filter. + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + List of task info dicts. + """ + if repo_root is None: + repo_root = get_repo_root() + + tasks_dir = get_tasks_dir(repo_root) + results = [] + + for t in iter_active_tasks(tasks_dir): + if (t.assignee or "-") != assignee: + continue + if filter_status and t.status != filter_status: + continue + results.append(_task_to_dict(t)) + + return results + + +def list_my_tasks( + filter_status: str | None = None, + repo_root: Path | None = None +) -> list[dict]: + """List tasks assigned to current developer. + + Args: + filter_status: Optional status filter. + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + List of task info dicts. + + Raises: + ValueError: If developer not set. + """ + if repo_root is None: + repo_root = get_repo_root() + + developer = get_developer(repo_root) + if not developer: + raise ValueError("Developer not set") + + return list_tasks_by_assignee(developer, filter_status, repo_root) + + +def get_task_stats(repo_root: Path | None = None) -> dict[str, int]: + """Get task statistics. + + Args: + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Dict with keys: P0, P1, P2, P3, Total. + """ + if repo_root is None: + repo_root = get_repo_root() + + tasks_dir = get_tasks_dir(repo_root) + stats = {"P0": 0, "P1": 0, "P2": 0, "P3": 0, "Total": 0} + + for t in iter_active_tasks(tasks_dir): + if t.priority in stats: + stats[t.priority] += 1 + stats["Total"] += 1 + + return stats + + +def format_task_stats(stats: dict[str, int]) -> str: + """Format task stats as string. + + Args: + stats: Stats dict from get_task_stats. + + Returns: + Formatted string like "P0:0 P1:1 P2:2 P3:0 Total:3". + """ + return f"P0:{stats['P0']} P1:{stats['P1']} P2:{stats['P2']} P3:{stats['P3']} Total:{stats['Total']}" + + +# ============================================================================= +# Main Entry (for testing) +# ============================================================================= + +if __name__ == "__main__": + stats = get_task_stats() + print(format_task_stats(stats)) + print() + print("Pending tasks:") + for task in list_pending_tasks(): + print(f" {task['priority']}|{task['id']}|{task['title']}|{task['status']}|{task['assignee']}") diff --git a/.trellis/scripts/common/task_store.py b/.trellis/scripts/common/task_store.py new file mode 100755 index 0000000..bed8f34 --- /dev/null +++ b/.trellis/scripts/common/task_store.py @@ -0,0 +1,747 @@ +#!/usr/bin/env python3 +""" +Task CRUD operations. + +Provides: + ensure_tasks_dir - Ensure tasks directory exists + cmd_create - Create a new task + cmd_archive - Archive completed task + cmd_set_branch - Set git branch for task + cmd_set_base_branch - Set PR target branch + cmd_set_scope - Set scope for PR title + cmd_add_subtask - Link child task to parent + cmd_remove_subtask - Unlink child task from parent +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from datetime import datetime +from pathlib import Path + +from .config import ( + get_packages, + get_session_auto_commit, + is_monorepo, + resolve_package, + validate_package, +) +from .git import run_git +from .io import read_json, write_json +from .log import Colors, colored +from .paths import ( + DIR_ARCHIVE, + DIR_TASKS, + DIR_WORKFLOW, + FILE_TASK_JSON, + generate_task_date_prefix, + get_developer, + get_repo_root, + get_tasks_dir, +) +from .safe_commit import ( + print_gitignore_warning, + safe_archive_paths_to_add, + safe_git_add, +) +from .task_utils import ( + archive_task_complete, + find_task_by_name, + resolve_task_dir, + run_task_hooks, +) + + +# ============================================================================= +# Helper Functions +# ============================================================================= + +def _slugify(title: str) -> str: + """Convert title to slug (only works with ASCII).""" + result = title.lower() + result = re.sub(r"[^a-z0-9]", "-", result) + result = re.sub(r"-+", "-", result) + result = result.strip("-") + return result + + +def ensure_tasks_dir(repo_root: Path) -> Path: + """Ensure tasks directory exists.""" + tasks_dir = get_tasks_dir(repo_root) + archive_dir = tasks_dir / "archive" + + if not tasks_dir.exists(): + tasks_dir.mkdir(parents=True) + print(colored(f"Created tasks directory: {tasks_dir}", Colors.GREEN), file=sys.stderr) + + if not archive_dir.exists(): + archive_dir.mkdir(parents=True) + + return tasks_dir + + +def _find_archived_task_by_dir_name(tasks_dir: Path, dir_name: str) -> Path | None: + """Find an archived task directory with the exact active-task dir name.""" + archive_dir = tasks_dir / DIR_ARCHIVE + if not archive_dir.is_dir(): + return None + + for month_dir in sorted(archive_dir.iterdir()): + if not month_dir.is_dir(): + continue + candidate = month_dir / dir_name + if candidate.is_dir(): + return candidate + + return None + + +def _repo_relative_path(path: Path, repo_root: Path) -> str: + """Format a path relative to the repo root when possible.""" + try: + return path.relative_to(repo_root).as_posix() + except ValueError: + return str(path) + + +# ============================================================================= +# Sub-agent platform detection + JSONL seeding +# ============================================================================= + +# Config directories of platforms that consume implement.jsonl / check.jsonl. +# Keep in sync with src/types/ai-tools.ts AI_TOOLS entries — these are the +# platforms listed in workflow.md's "agent-capable" Skill Routing block +# (Class-1 hook-inject + Class-2 pull-based preludes). Kilo / Antigravity / +# Devin are NOT in this list: they do not consume JSONL. +_SUBAGENT_CONFIG_DIRS: tuple[str, ...] = ( + ".claude", + ".cursor", + ".codex", + ".kiro", + ".gemini", + ".opencode", + ".qoder", + ".codebuddy", + ".factory", # Factory Droid + ".github/copilot", + ".pi", # Pi Agent + ".trae", # Trae IDE +) + +_SEED_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." +) + + +def _has_subagent_platform(repo_root: Path) -> bool: + """Return True if any sub-agent-capable platform is configured. + + Detected by probing well-known config directories at the repo root. Used + only to decide whether ``task.py create`` should seed empty + ``implement.jsonl`` / ``check.jsonl`` files. + """ + for config_dir in _SUBAGENT_CONFIG_DIRS: + if (repo_root / config_dir).is_dir(): + return True + return False + + +def _write_seed_jsonl(path: Path) -> None: + """Write a one-line seed JSONL file with a self-describing ``_example``. + + The seed row has no ``file`` field, so downstream consumers (hooks + + preludes) that iterate entries via ``item.get("file")`` naturally skip + it. The row exists purely as an in-file prompt for the AI curator. + """ + seed = {"_example": _SEED_EXAMPLE} + path.write_text(json.dumps(seed, ensure_ascii=False) + "\n", encoding="utf-8") + + +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 +# ============================================================================= + +def cmd_create(args: argparse.Namespace) -> int: + """Create a new task.""" + repo_root = get_repo_root() + + if not args.title: + print(colored("Error: title is required", Colors.RED), file=sys.stderr) + return 1 + + # Validate --package (CLI source: fail-fast) + package: str | None = getattr(args, "package", None) + if not is_monorepo(repo_root): + # Single-repo: ignore --package, no package prefix + if package: + print(colored(f"Warning: --package ignored in single-repo project", Colors.YELLOW), file=sys.stderr) + package = None + elif package: + if not validate_package(package, repo_root): + packages = get_packages(repo_root) + available = ", ".join(sorted(packages.keys())) if packages else "(none)" + print(colored(f"Error: unknown package '{package}'. Available: {available}", Colors.RED), file=sys.stderr) + return 1 + else: + # Inferred: default_package → None (no task.json yet for create) + package = resolve_package(repo_root=repo_root) + + # Default assignee to current developer + assignee = args.assignee + if not assignee: + assignee = get_developer(repo_root) + if not assignee: + print(colored("Error: No developer set. Run init_developer.py first or use --assignee", Colors.RED), file=sys.stderr) + return 1 + + ensure_tasks_dir(repo_root) + + # Get current developer as creator + creator = get_developer(repo_root) or assignee + + # Generate slug if not provided + slug = args.slug or _slugify(args.title) + if not slug: + print(colored("Error: could not generate slug from title", Colors.RED), file=sys.stderr) + return 1 + + # Create task directory with MM-DD-slug format + tasks_dir = get_tasks_dir(repo_root) + date_prefix = generate_task_date_prefix() + dir_name = f"{date_prefix}-{slug}" + task_dir = tasks_dir / dir_name + task_json_path = task_dir / FILE_TASK_JSON + + archived_task_dir = _find_archived_task_by_dir_name(tasks_dir, dir_name) + if archived_task_dir: + print(colored(f"Error: Task already archived: {dir_name}", Colors.RED), file=sys.stderr) + print(f"Archived at: {_repo_relative_path(archived_task_dir, repo_root)}", file=sys.stderr) + print("Use a new slug if you intend to create a new task.", file=sys.stderr) + return 1 + + if task_dir.exists(): + print(colored(f"Warning: Task directory already exists: {dir_name}", Colors.YELLOW), file=sys.stderr) + else: + task_dir.mkdir(parents=True) + + today = datetime.now().strftime("%Y-%m-%d") + + # Record current branch as base_branch (PR target) + _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root) + current_branch = branch_out.strip() or "main" + + task_data = { + "id": slug, + "name": slug, + "title": args.title, + "description": args.description or "", + "status": "planning", + "dev_type": None, + "scope": None, + "package": package, + "priority": args.priority, + "creator": creator, + "assignee": assignee, + "createdAt": today, + "completedAt": None, + "branch": None, + "base_branch": current_branch, + "worktree_path": None, + "commit": None, + "pr_url": None, + "subtasks": [], + "children": [], + "parent": None, + "relatedFiles": [], + "notes": "", + "meta": {}, + } + + write_json(task_json_path, task_data) + + prd_path = task_dir / "prd.md" + if not prd_path.exists(): + prd_path.write_text( + _default_prd_content(args.title, args.description), + encoding="utf-8", + ) + + # Seed implement.jsonl / check.jsonl for sub-agent-capable platforms. + # 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): + for jsonl_name in ("implement.jsonl", "check.jsonl"): + jsonl_path = task_dir / jsonl_name + if not jsonl_path.exists(): + _write_seed_jsonl(jsonl_path) + seeded_jsonl = True + + # Handle --parent: establish bidirectional link + if args.parent: + parent_dir = resolve_task_dir(args.parent, repo_root) + parent_json_path = parent_dir / FILE_TASK_JSON + if not parent_json_path.is_file(): + print(colored(f"Warning: Parent task.json not found: {args.parent}", Colors.YELLOW), file=sys.stderr) + else: + parent_data = read_json(parent_json_path) + if parent_data: + # Add child to parent's children list + parent_children = parent_data.get("children", []) + if dir_name not in parent_children: + parent_children.append(dir_name) + parent_data["children"] = parent_children + write_json(parent_json_path, parent_data) + + # Set parent in child's task.json + task_data["parent"] = parent_dir.name + write_json(task_json_path, task_data) + + print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr) + + # Auto-activate the new task so the per-turn breadcrumb fires planning + # state. Best-effort: gracefully degrade if no session identity (CLI run + # outside an AI session) — the task is still created, the user can run + # task.py start later. Pointer is session-scoped so this never affects + # other AI sessions. + try: + from .active_task import resolve_context_key, set_active_task + if resolve_context_key(): + try: + rel_dir = task_dir.relative_to(repo_root).as_posix() + except ValueError: + rel_dir = str(task_dir) + set_active_task(rel_dir, repo_root) + except Exception: + pass + + print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr) + print("", file=sys.stderr) + print(colored("Next steps:", Colors.BLUE), file=sys.stderr) + print(" - 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( + " - Curate implement.jsonl / check.jsonl as spec/research manifests when sub-agents need context", + 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 + print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}") + + run_task_hooks("after_create", task_json_path, repo_root) + return 0 + + +# ============================================================================= +# Command: archive +# ============================================================================= + +def cmd_archive(args: argparse.Namespace) -> int: + """Archive completed task.""" + repo_root = get_repo_root() + task_name = args.name + + if not task_name: + print(colored("Error: Task name is required", Colors.RED), file=sys.stderr) + return 1 + + tasks_dir = get_tasks_dir(repo_root) + + # Resolve task directory (supports task name, relative path, or absolute path) + task_dir = resolve_task_dir(task_name, repo_root) + + if not task_dir or not task_dir.is_dir(): + print(colored(f"Error: Task not found: {task_name}", Colors.RED), file=sys.stderr) + print("Active tasks:", file=sys.stderr) + # Import lazily to avoid circular dependency + from .tasks import iter_active_tasks + for t in iter_active_tasks(tasks_dir): + print(f" - {t.dir_name}/", file=sys.stderr) + return 1 + + dir_name = task_dir.name + task_json_path = task_dir / FILE_TASK_JSON + + # Update status before archiving + today = datetime.now().strftime("%Y-%m-%d") + # Names of child task dirs whose task.json gets modified below; passed + # into safe_archive_paths_to_add so they're staged in this commit. + modified_children: list[str] = [] + if task_json_path.is_file(): + data = read_json(task_json_path) + if data: + data["status"] = "completed" + data["completedAt"] = today + write_json(task_json_path, data) + + # Handle subtask relationships on archive. + # Keep this task in its parent's children list so progress + # counters (children_progress) stay consistent — children + # missing from the active set are treated as completed. + task_children = data.get("children", []) + + # If this is a parent, clear parent field in all children + if task_children: + for child_name in task_children: + child_dir_path = find_task_by_name(child_name, tasks_dir) + if child_dir_path: + child_json = child_dir_path / FILE_TASK_JSON + if child_json.is_file(): + child_data = read_json(child_json) + if child_data: + child_data["parent"] = None + write_json(child_json, child_data) + modified_children.append(child_dir_path.name) + + # Clear any session that still points at this task before the path moves. + from .active_task import clear_task_from_sessions + clear_task_from_sessions(str(task_dir), repo_root) + + # Archive + result = archive_task_complete(task_dir, repo_root) + if "archived_to" in result: + archive_dest = Path(result["archived_to"]) + year_month = archive_dest.parent.name + print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr) + + # Auto-commit unless --no-commit + if not getattr(args, "no_commit", False): + 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}") + + # Run hooks with the archived path + archived_json = archive_dest / FILE_TASK_JSON + run_task_hooks("after_archive", archived_json, repo_root) + return 0 + + return 1 + + +def _auto_commit_archive( + task_name: str, + repo_root: Path, + modified_children: list[str] | None = None, +) -> bool: + """Stage Trellis-owned task paths and commit after archive. + + Scoped narrowly to the archived task's source + destination paths + plus any child task dirs whose ``task.json`` was edited (parent → + children relationship update). Dirty changes in OTHER active task + dirs are NOT bundled into the archive commit. + + If ``.gitignore`` blocks the paths, we warn + skip — we do NOT + retry with ``git add -f``. The warning explicitly forbids + ``git add -f .trellis/`` (which would fan out to caches/backups) + and points users at ``session_auto_commit: false``. + + Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when + set to ``false``, this function returns immediately without + touching git (the archive directory move on disk is unaffected). + """ + if not get_session_auto_commit(repo_root): + print( + "[OK] session_auto_commit: false — skipping git stage/commit.", + file=sys.stderr, + ) + 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 True + + success, _, err = safe_git_add(paths, repo_root) + if not success: + if err and "ignored by" in err.lower(): + print_gitignore_warning(paths) + else: + print( + f"[WARN] git add failed: {err.strip() if err else 'unknown error'}", + file=sys.stderr, + ) + 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 + # source task directory was moved away by `shutil.move`, so its files + # need an explicit `git rm --cached` to stage the deletions in this + # same commit — otherwise they sit as uncommitted "phantom deletes" + # against HEAD until something later picks them up. + # + # `--ignore-unmatch` makes this a no-op when the task was never tracked + # (e.g. archiving a task that lived only in working tree). + run_git( + ["rm", "-r", "--cached", "--ignore-unmatch", "--", source_rel], + cwd=repo_root, + ) + + rc, _, _ = run_git( + ["diff", "--cached", "--quiet", "--", *paths, source_rel], + cwd=repo_root, + ) + if rc == 0: + print("[OK] No task changes to commit.", file=sys.stderr) + 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 + + +# ============================================================================= +# Command: add-subtask +# ============================================================================= + +def cmd_add_subtask(args: argparse.Namespace) -> int: + """Link a child task to a parent task.""" + repo_root = get_repo_root() + + parent_dir = resolve_task_dir(args.parent_dir, repo_root) + child_dir = resolve_task_dir(args.child_dir, repo_root) + + parent_json_path = parent_dir / FILE_TASK_JSON + child_json_path = child_dir / FILE_TASK_JSON + + if not parent_json_path.is_file(): + print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr) + return 1 + + if not child_json_path.is_file(): + print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr) + return 1 + + parent_data = read_json(parent_json_path) + child_data = read_json(child_json_path) + + if not parent_data or not child_data: + print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr) + return 1 + + # Check if child already has a parent + existing_parent = child_data.get("parent") + if existing_parent: + print(colored(f"Error: Child task already has a parent: {existing_parent}", Colors.RED), file=sys.stderr) + return 1 + + # Add child to parent's children list + parent_children = parent_data.get("children", []) + child_dir_name = child_dir.name + if child_dir_name not in parent_children: + parent_children.append(child_dir_name) + parent_data["children"] = parent_children + + # Set parent in child's task.json + child_data["parent"] = parent_dir.name + + # Write both + write_json(parent_json_path, parent_data) + write_json(child_json_path, child_data) + + print(colored(f"Linked: {child_dir.name} -> {parent_dir.name}", Colors.GREEN), file=sys.stderr) + return 0 + + +# ============================================================================= +# Command: remove-subtask +# ============================================================================= + +def cmd_remove_subtask(args: argparse.Namespace) -> int: + """Unlink a child task from a parent task.""" + repo_root = get_repo_root() + + parent_dir = resolve_task_dir(args.parent_dir, repo_root) + child_dir = resolve_task_dir(args.child_dir, repo_root) + + parent_json_path = parent_dir / FILE_TASK_JSON + child_json_path = child_dir / FILE_TASK_JSON + + if not parent_json_path.is_file(): + print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr) + return 1 + + if not child_json_path.is_file(): + print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr) + return 1 + + parent_data = read_json(parent_json_path) + child_data = read_json(child_json_path) + + if not parent_data or not child_data: + print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr) + return 1 + + # Remove child from parent's children list + parent_children = parent_data.get("children", []) + child_dir_name = child_dir.name + if child_dir_name in parent_children: + parent_children.remove(child_dir_name) + parent_data["children"] = parent_children + + # Clear parent in child's task.json + child_data["parent"] = None + + # Write both + write_json(parent_json_path, parent_data) + write_json(child_json_path, child_data) + + print(colored(f"Unlinked: {child_dir.name} from {parent_dir.name}", Colors.GREEN), file=sys.stderr) + return 0 + + +# ============================================================================= +# Command: set-branch +# ============================================================================= + +def cmd_set_branch(args: argparse.Namespace) -> int: + """Set git branch for task.""" + repo_root = get_repo_root() + target_dir = resolve_task_dir(args.dir, repo_root) + branch = args.branch + + if not branch: + print(colored("Error: Missing arguments", Colors.RED)) + print("Usage: python3 task.py set-branch <task-dir> <branch-name>") + return 1 + + task_json = target_dir / FILE_TASK_JSON + if not task_json.is_file(): + print(colored(f"Error: task.json not found at {target_dir}", Colors.RED)) + return 1 + + data = read_json(task_json) + if not data: + return 1 + + data["branch"] = branch + write_json(task_json, data) + + print(colored(f"✓ Branch set to: {branch}", Colors.GREEN)) + return 0 + + +# ============================================================================= +# Command: set-base-branch +# ============================================================================= + +def cmd_set_base_branch(args: argparse.Namespace) -> int: + """Set the base branch (PR target) for task.""" + repo_root = get_repo_root() + target_dir = resolve_task_dir(args.dir, repo_root) + base_branch = args.base_branch + + if not base_branch: + print(colored("Error: Missing arguments", Colors.RED)) + print("Usage: python3 task.py set-base-branch <task-dir> <base-branch>") + print("Example: python3 task.py set-base-branch <dir> develop") + print() + print("This sets the target branch for PR (the branch your feature will merge into).") + return 1 + + task_json = target_dir / FILE_TASK_JSON + if not task_json.is_file(): + print(colored(f"Error: task.json not found at {target_dir}", Colors.RED)) + return 1 + + data = read_json(task_json) + if not data: + return 1 + + data["base_branch"] = base_branch + write_json(task_json, data) + + print(colored(f"✓ Base branch set to: {base_branch}", Colors.GREEN)) + print(f" PR will target: {base_branch}") + return 0 + + +# ============================================================================= +# Command: set-scope +# ============================================================================= + +def cmd_set_scope(args: argparse.Namespace) -> int: + """Set scope for PR title.""" + repo_root = get_repo_root() + target_dir = resolve_task_dir(args.dir, repo_root) + scope = args.scope + + if not scope: + print(colored("Error: Missing arguments", Colors.RED)) + print("Usage: python3 task.py set-scope <task-dir> <scope>") + return 1 + + task_json = target_dir / FILE_TASK_JSON + if not task_json.is_file(): + print(colored(f"Error: task.json not found at {target_dir}", Colors.RED)) + return 1 + + data = read_json(task_json) + if not data: + return 1 + + data["scope"] = scope + write_json(task_json, data) + + print(colored(f"✓ Scope set to: {scope}", Colors.GREEN)) + return 0 diff --git a/.trellis/scripts/common/task_utils.py b/.trellis/scripts/common/task_utils.py new file mode 100755 index 0000000..62c215e --- /dev/null +++ b/.trellis/scripts/common/task_utils.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +""" +Task utility functions. + +Provides: + is_safe_task_path - Validate task path is safe to operate on + find_task_by_name - Find task directory by name + resolve_task_dir - Resolve task directory from name, relative, or absolute path + archive_task_dir - Archive task to monthly directory + run_task_hooks - Run lifecycle hooks for task events +""" + +from __future__ import annotations + +import shutil +import sys +from datetime import datetime +from pathlib import Path + +from .paths import get_repo_root, get_tasks_dir + + +# ============================================================================= +# Path Safety +# ============================================================================= + +def is_safe_task_path(task_path: str, repo_root: Path | None = None) -> bool: + """Check if a relative task path is safe to operate on. + + Args: + task_path: Task path (relative to repo_root). + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + True if safe, False if dangerous. + """ + if repo_root is None: + repo_root = get_repo_root() + + normalized = task_path.replace("\\", "/") + + # Check empty or null + if not normalized or normalized == "null": + print("Error: empty or null task path", file=sys.stderr) + return False + + # Reject absolute paths + if Path(task_path).is_absolute(): + print(f"Error: absolute path not allowed: {task_path}", file=sys.stderr) + return False + + # Reject ".", "..", paths starting with "./" or "../", or containing ".." + if normalized in (".", "..") or normalized.startswith("./") or normalized.startswith("../") or ".." in normalized: + print(f"Error: path traversal not allowed: {task_path}", file=sys.stderr) + return False + + # Final check: ensure resolved path is not the repo root + abs_path = repo_root / Path(normalized) + if abs_path.exists(): + try: + resolved = abs_path.resolve() + root_resolved = repo_root.resolve() + if resolved == root_resolved: + print(f"Error: path resolves to repo root: {task_path}", file=sys.stderr) + return False + except (OSError, IOError): + pass + + return True + + +# ============================================================================= +# Task Lookup +# ============================================================================= + +def find_task_by_name(task_name: str, tasks_dir: Path) -> Path | None: + """Find task directory by name (exact or suffix match). + + Args: + task_name: Task name to find. + tasks_dir: Tasks directory path. + + Returns: + Absolute path to task directory, or None if not found. + """ + if not task_name or not tasks_dir or not tasks_dir.is_dir(): + return None + + # Try exact match first + exact_match = tasks_dir / task_name + if exact_match.is_dir(): + return exact_match + + # Try suffix match (e.g., "my-task" matches "01-21-my-task") + for d in tasks_dir.iterdir(): + if d.is_dir() and d.name.endswith(f"-{task_name}"): + return d + + return None + + +# ============================================================================= +# Archive Operations +# ============================================================================= + +def archive_task_dir(task_dir_abs: Path, repo_root: Path | None = None) -> Path | None: + """Archive a task directory to archive/{YYYY-MM}/. + + Args: + task_dir_abs: Absolute path to task directory. + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Path to archived directory, or None on error. + """ + if not task_dir_abs.is_dir(): + print(f"Error: task directory not found: {task_dir_abs}", file=sys.stderr) + return None + + # Get tasks directory (parent of the task) + tasks_dir = task_dir_abs.parent + archive_dir = tasks_dir / "archive" + year_month = datetime.now().strftime("%Y-%m") + month_dir = archive_dir / year_month + + # Create archive directory + try: + month_dir.mkdir(parents=True, exist_ok=True) + except (OSError, IOError) as e: + print(f"Error: Failed to create archive directory: {e}", file=sys.stderr) + return None + + # Move task to archive + task_name = task_dir_abs.name + dest = month_dir / task_name + + try: + shutil.move(str(task_dir_abs), str(dest)) + except (OSError, IOError, shutil.Error) as e: + print(f"Error: Failed to move task to archive: {e}", file=sys.stderr) + return None + + return dest + + +def archive_task_complete( + task_dir_abs: Path, + repo_root: Path | None = None +) -> dict[str, str]: + """Complete archive workflow: archive directory. + + Args: + task_dir_abs: Absolute path to task directory. + repo_root: Repository root path. Defaults to auto-detected. + + Returns: + Dict with archive result info. + """ + if not task_dir_abs.is_dir(): + print(f"Error: task directory not found: {task_dir_abs}", file=sys.stderr) + return {} + + archive_dest = archive_task_dir(task_dir_abs, repo_root) + if archive_dest: + return {"archived_to": str(archive_dest)} + + return {} + + +# ============================================================================= +# Task Directory Resolution +# ============================================================================= + +def resolve_task_dir(target_dir: str, repo_root: Path) -> Path: + """Resolve task directory to absolute path. + + Supports: + - Absolute path: /path/to/task + - Relative path: .trellis/tasks/01-31-my-task + - Task name: my-task (uses find_task_by_name for lookup) + + Args: + target_dir: Task directory specification. + repo_root: Repository root path. + + Returns: + Resolved absolute path. + """ + if not target_dir: + return Path() + + normalized = target_dir.replace("\\", "/") + while normalized.startswith("./"): + normalized = normalized[2:] + + # Absolute path + if Path(target_dir).is_absolute(): + return Path(target_dir) + + # Relative path (contains path separator or starts with .trellis) + if "/" in normalized or normalized.startswith(".trellis"): + return repo_root / Path(normalized) + + # Task name - try to find in tasks directory + tasks_dir = get_tasks_dir(repo_root) + found = find_task_by_name(target_dir, tasks_dir) + if found: + return found + + # Fallback to treating as relative path + return repo_root / Path(normalized) + + +# ============================================================================= +# Lifecycle Hooks +# ============================================================================= + +def run_task_hooks(event: str, task_json_path: Path, repo_root: Path) -> None: + """Run lifecycle hooks for a task event. + + Args: + event: Event name (e.g. "after_create"). + task_json_path: Absolute path to the task's task.json. + repo_root: Repository root for cwd and config lookup. + """ + import os + import subprocess + + from .config import get_hooks + from .log import Colors, colored + + commands = get_hooks(event, repo_root) + if not commands: + return + + env = {**os.environ, "TASK_JSON_PATH": str(task_json_path)} + + for cmd in commands: + try: + result = subprocess.run( + cmd, + shell=True, + cwd=repo_root, + env=env, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + if result.returncode != 0: + print( + colored(f"[WARN] Hook failed ({event}): {cmd}", Colors.YELLOW), + file=sys.stderr, + ) + if result.stderr.strip(): + print(f" {result.stderr.strip()}", file=sys.stderr) + except Exception as e: + print( + colored(f"[WARN] Hook error ({event}): {cmd} — {e}", Colors.YELLOW), + file=sys.stderr, + ) + + +# ============================================================================= +# Main Entry (for testing) +# ============================================================================= + +if __name__ == "__main__": + repo = get_repo_root() + tasks = get_tasks_dir(repo) + + print(f"Tasks dir: {tasks}") + print(f"is_safe_task_path('.trellis/tasks/test'): {is_safe_task_path('.trellis/tasks/test', repo)}") + print(f"is_safe_task_path('../test'): {is_safe_task_path('../test', repo)}") diff --git a/.trellis/scripts/common/tasks.py b/.trellis/scripts/common/tasks.py new file mode 100755 index 0000000..7b44094 --- /dev/null +++ b/.trellis/scripts/common/tasks.py @@ -0,0 +1,112 @@ +""" +Task data access layer. + +Single source of truth for loading and iterating task directories. +Replaces scattered task.json parsing across 9+ files. + +Provides: + load_task — Load a single task by directory path + iter_active_tasks — Iterate all non-archived tasks (sorted) + get_all_statuses — Get {dir_name: status} map for children progress +""" + +from __future__ import annotations + +from collections.abc import Iterator +from pathlib import Path + +from .io import read_json +from .paths import FILE_TASK_JSON +from .types import TaskInfo + + +def load_task(task_dir: Path) -> TaskInfo | None: + """Load task from a directory containing task.json. + + Args: + task_dir: Absolute path to the task directory. + + Returns: + TaskInfo if task.json exists and is valid, None otherwise. + """ + task_json = task_dir / FILE_TASK_JSON + if not task_json.is_file(): + return None + + data = read_json(task_json) + if not data: + return None + + return TaskInfo( + dir_name=task_dir.name, + directory=task_dir, + title=data.get("title") or data.get("name") or "unknown", + status=data.get("status", "unknown"), + assignee=data.get("assignee", ""), + priority=data.get("priority", "P2"), + children=tuple(data.get("children", [])), + parent=data.get("parent"), + package=data.get("package"), + raw=data, + ) + + +def iter_active_tasks(tasks_dir: Path) -> Iterator[TaskInfo]: + """Iterate all active (non-archived) tasks, sorted by directory name. + + Skips the "archive" directory and directories without valid task.json. + + Args: + tasks_dir: Path to the tasks directory. + + Yields: + TaskInfo for each valid task. + """ + if not tasks_dir.is_dir(): + return + + for d in sorted(tasks_dir.iterdir()): + if not d.is_dir() or d.name == "archive": + continue + info = load_task(d) + if info is not None: + yield info + + +def get_all_statuses(tasks_dir: Path) -> dict[str, str]: + """Get a {dir_name: status} mapping for all active tasks. + + Useful for computing children progress without loading full TaskInfo. + + Args: + tasks_dir: Path to the tasks directory. + + Returns: + Dict mapping directory names to status strings. + """ + return {t.dir_name: t.status for t in iter_active_tasks(tasks_dir)} + + +def children_progress( + children: tuple[str, ...] | list[str], + all_statuses: dict[str, str], +) -> str: + """Format children progress string like " [2/3 done]". + + Args: + children: List of child directory names. + all_statuses: Status map from get_all_statuses(). + + Returns: + Formatted string, or "" if no children. + """ + if not children: + return "" + # A child missing from active statuses has been archived (cmd_archive + # sets status=completed before moving the dir). Count it as done so + # parent progress doesn't regress when children are archived. + done = sum( + 1 for c in children + if c not in all_statuses or all_statuses.get(c) in ("completed", "done") + ) + return f" [{done}/{len(children)} done]" diff --git a/.trellis/scripts/common/trellis_config.py b/.trellis/scripts/common/trellis_config.py new file mode 100755 index 0000000..5dbec7a --- /dev/null +++ b/.trellis/scripts/common/trellis_config.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +Standalone reader for .trellis/config.yaml. + +Mirrors a minimal subset of common.config so callers (hooks, workflow_phase) +can read configuration without importing the full task/repo helpers. Returns +an empty dict on missing/malformed files so callers stay simple. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Optional + + +CONFIG_REL_PATH = ".trellis/config.yaml" + + +def _unquote(value: str) -> str: + if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"): + return value[1:-1] + return value + + +def _strip_inline_comment(value: str) -> str: + """Strip ` # …` inline comments while preserving `#` inside quoted strings. + + YAML treats ` #` (space-hash) as a comment opener; bare `#` inside a token + is part of the value. Quoted strings are immune. + """ + in_quote: str | None = None + for idx, ch in enumerate(value): + if in_quote: + if ch == in_quote: + in_quote = None + continue + if ch in ('"', "'"): + in_quote = ch + continue + if ch == "#" and (idx == 0 or value[idx - 1].isspace()): + return value[:idx] + return value + + +def _next_content_line(lines: list[str], start: int) -> tuple[int, str]: + i = start + while i < len(lines): + stripped = lines[i].strip() + if stripped and not stripped.startswith("#"): + return i, lines[i] + i += 1 + return i, "" + + +def _parse_yaml_block( + lines: list[str], start: int, min_indent: int, target: dict +) -> int: + i = start + current_list: list | None = None + + while i < len(lines): + line = lines[i] + stripped = line.strip() + + if not stripped or stripped.startswith("#"): + i += 1 + continue + + indent = len(line) - len(line.lstrip()) + if indent < min_indent: + break + + if stripped.startswith("- "): + if current_list is not None: + current_list.append(_unquote(stripped[2:].strip())) + i += 1 + elif ":" in stripped: + key, _, value = stripped.partition(":") + key = key.strip() + value = _strip_inline_comment(value).strip() + value = _unquote(value) + current_list = None + + if value: + target[key] = value + i += 1 + else: + next_i, next_line = _next_content_line(lines, i + 1) + if next_i >= len(lines): + target[key] = {} + i = next_i + elif next_line.strip().startswith("- "): + current_list = [] + target[key] = current_list + i += 1 + else: + next_indent = len(next_line) - len(next_line.lstrip()) + if next_indent > indent: + nested: dict = {} + target[key] = nested + i = _parse_yaml_block(lines, i + 1, next_indent, nested) + else: + target[key] = {} + i += 1 + else: + i += 1 + + return i + + +def parse_simple_yaml(content: str) -> dict: + """Parse a small subset of YAML. See common.config for full doc.""" + lines = content.splitlines() + result: dict = {} + _parse_yaml_block(lines, 0, 0, result) + return result + + +def read_trellis_config(repo_root: Optional[Path] = None) -> dict: + """Read .trellis/config.yaml. Returns {} on missing or malformed file.""" + root = repo_root or Path.cwd() + config_file = root / CONFIG_REL_PATH + try: + content = config_file.read_text(encoding="utf-8") + except (FileNotFoundError, OSError): + return {} + try: + parsed = parse_simple_yaml(content) + except Exception: + return {} + return parsed if isinstance(parsed, dict) else {} diff --git a/.trellis/scripts/common/types.py b/.trellis/scripts/common/types.py new file mode 100755 index 0000000..5802e10 --- /dev/null +++ b/.trellis/scripts/common/types.py @@ -0,0 +1,110 @@ +""" +Core type definitions for Trellis task data. + +Provides: + TaskData — TypedDict for task.json shape (read-path type hints only) + TaskInfo — Frozen dataclass for loaded task (the public API type) + AgentRecord — TypedDict for registry.json agent entries +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import TypedDict + + +# ============================================================================= +# task.json shape (TypedDict — used only for read-path type hints) +# ============================================================================= + +class TaskData(TypedDict, total=False): + """Shape of task.json on disk. + + Used only for type annotations when reading task.json. + Writes must use the original dict to avoid losing unknown fields. + """ + + id: str + name: str + title: str + description: str + status: str + dev_type: str + scope: str | None + package: str | None + priority: str + creator: str + assignee: str + createdAt: str + completedAt: str | None + branch: str | None + base_branch: str | None + worktree_path: str | None + commit: str | None + pr_url: str | None + subtasks: list[str] + children: list[str] + parent: str | None + relatedFiles: list[str] + notes: str + meta: dict + + +# ============================================================================= +# Loaded task object (frozen dataclass — the public API type) +# ============================================================================= + +@dataclass(frozen=True) +class TaskInfo: + """Immutable view of a loaded task. + + Created by load_task() / iter_active_tasks(). + Contains the commonly accessed fields; the original dict + is preserved in `raw` for write-back and uncommon field access. + """ + + dir_name: str + directory: Path + title: str + status: str + assignee: str + priority: str + children: tuple[str, ...] + parent: str | None + package: str | None + raw: dict # original dict — use for writes and uncommon fields + + @property + def name(self) -> str: + """Task name (id or name field).""" + return self.raw.get("name") or self.raw.get("id") or self.dir_name + + @property + def description(self) -> str: + return self.raw.get("description", "") + + @property + def branch(self) -> str | None: + return self.raw.get("branch") + + @property + def meta(self) -> dict: + return self.raw.get("meta", {}) + + +# ============================================================================= +# registry.json agent entry +# ============================================================================= + +class AgentRecord(TypedDict, total=False): + """Shape of an agent entry in registry.json.""" + + id: str + pid: int + task_dir: str + worktree_path: str + branch: str + platform: str + started_at: str + status: str diff --git a/.trellis/scripts/common/workflow_phase.py b/.trellis/scripts/common/workflow_phase.py new file mode 100755 index 0000000..015448d --- /dev/null +++ b/.trellis/scripts/common/workflow_phase.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Workflow Phase Extraction. + +Extracts step-level content from .trellis/workflow.md and optionally filters +platform-specific blocks. + +Platform marker syntax in workflow.md: + + [Claude Code, Cursor, ...] + agent-capable content + [/Claude Code, Cursor, ...] + +Provides: + get_phase_index - Extract the Phase Index section (no --step) + get_step - Extract a single step (#### X.X) section + filter_platform - Strip platform blocks that don't include the given name +""" + +from __future__ import annotations + +import re + +from .paths import DIR_WORKFLOW, get_repo_root + + +def _workflow_md_path(): + return get_repo_root() / DIR_WORKFLOW / "workflow.md" + +# Match a line that *is* a platform marker: "[A, B, C]" or "[/A, B, C]" +_MARKER_RE = re.compile(r"^\[(/?)([A-Za-z][^\[\]]*)\]\s*$") + +# Step heading: "#### 1.0 Title" or "#### 1.0 ..." +_STEP_HEADING_RE = re.compile(r"^####\s+(\d+\.\d+)\b.*$") + +# Phase Index starts here; Phase 1/2/3 step bodies follow; ends at Breadcrumbs. +_PHASE_INDEX_HEADING = "## Phase Index" + + +def _read_workflow() -> str: + path = _workflow_md_path() + if not path.exists(): + raise FileNotFoundError(f"workflow.md not found: {path}") + return path.read_text(encoding="utf-8") + + +def _parse_marker(line: str) -> tuple[bool, list[str]] | None: + """Parse a platform marker line. + + Returns: + (is_closing, [platform_names]) if line is a marker, else None. + """ + m = _MARKER_RE.match(line) + if not m: + return None + is_closing = m.group(1) == "/" + names = [p.strip() for p in m.group(2).split(",") if p.strip()] + return is_closing, names + + +def get_phase_index() -> str: + """Return the compact Phase Index summary from workflow.md. + + 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() + + start: int | None = None + end: int | None = None + for i, line in enumerate(lines): + stripped = line.strip() + if start is None and stripped == _PHASE_INDEX_HEADING: + start = i + continue + if start is not None and stripped == "## Phase 1: Plan": + end = i + break + + if start is None: + return "" + if end is None: + end = len(lines) + + section = "\n".join(lines[start:end]).rstrip() + # Strip [workflow-state:STATUS]...[/workflow-state:STATUS] blocks since + # they're injected separately by inject-workflow-state.py per-turn. + import re as _re + tag_re = _re.compile( + r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]\n?", + _re.DOTALL, + ) + return tag_re.sub("", section).rstrip() + "\n" + + +def get_step(step_id: str) -> str: + """Return the `#### X.X` section matching step_id (header + body). + + Body ends at the next `####` or `---` or `##` heading (whichever comes first). + """ + text = _read_workflow() + lines = text.splitlines() + + start: int | None = None + for i, line in enumerate(lines): + m = _STEP_HEADING_RE.match(line) + if m and m.group(1) == step_id: + start = i + break + if start is None: + return "" + + end: int = len(lines) + for j in range(start + 1, len(lines)): + line = lines[j] + if line.startswith("#### "): + end = j + break + if line.startswith("## "): + end = j + break + # Horizontal rule at column 0 + if line.strip() == "---": + end = j + break + + return "\n".join(lines[start:end]).rstrip() + "\n" + + +def _platform_matches(platform: str, block_names: list[str]) -> bool: + """Case-insensitive fuzzy match: accept 'cursor', 'Cursor', 'claude-code', 'Claude Code'.""" + needle = platform.lower().replace("-", "").replace("_", "").replace(" ", "") + for name in block_names: + hay = name.lower().replace("-", "").replace("_", "").replace(" ", "") + if needle == hay: + return True + return False + + +def 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``. + ``filter_platform`` then surfaces blocks whose marker lists include the + namespaced name (e.g. ``[codex-sub-agent, ...]`` or ``[codex-inline, Kilo, + 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. + + Other platforms are returned unchanged. + """ + if platform == "codex": + mode = "inline" + 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}" + return platform + + +def filter_platform(content: str, platform: str) -> str: + """Keep lines outside any `[...]` block + lines inside blocks that include platform. + + Marker lines themselves are dropped from the output. + """ + lines = content.splitlines() + out: list[str] = [] + + in_block = False + keep_block = False + + for line in lines: + marker = _parse_marker(line) + if marker is not None: + is_closing, names = marker + if not is_closing: + in_block = True + keep_block = _platform_matches(platform, names) + else: + in_block = False + keep_block = False + continue # drop the marker line itself + + if in_block: + if keep_block: + out.append(line) + continue + out.append(line) + + # Collapse runs of 3+ blank lines that may arise from dropped markers + collapsed: list[str] = [] + blank_run = 0 + for line in out: + if line.strip() == "": + blank_run += 1 + if blank_run <= 2: + collapsed.append(line) + else: + blank_run = 0 + collapsed.append(line) + + return "\n".join(collapsed).rstrip() + "\n" diff --git a/.trellis/scripts/get_context.py b/.trellis/scripts/get_context.py new file mode 100755 index 0000000..bc63463 --- /dev/null +++ b/.trellis/scripts/get_context.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +""" +Get Session Context for AI Agent. + +Usage: + python3 get_context.py Output context in text format + python3 get_context.py --json Output context in JSON format +""" + +from __future__ import annotations + +from common.git_context import main + + +if __name__ == "__main__": + main() diff --git a/.trellis/scripts/get_developer.py b/.trellis/scripts/get_developer.py new file mode 100755 index 0000000..f8a89eb --- /dev/null +++ b/.trellis/scripts/get_developer.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +""" +Get current developer name. + +This is a wrapper that uses common/paths.py +""" + +from __future__ import annotations + +import sys + +from common.paths import get_developer + + +def main() -> None: + """CLI entry point.""" + developer = get_developer() + if developer: + print(developer) + else: + print("Developer not initialized", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.trellis/scripts/hooks/linear_sync.py b/.trellis/scripts/hooks/linear_sync.py new file mode 100755 index 0000000..5659fde --- /dev/null +++ b/.trellis/scripts/hooks/linear_sync.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""Linear sync hook for Trellis task lifecycle. + +Syncs task events to Linear via the `linearis` CLI. + +Usage (called automatically by task.py hooks): + python3 .trellis/scripts/hooks/linear_sync.py create + python3 .trellis/scripts/hooks/linear_sync.py start + python3 .trellis/scripts/hooks/linear_sync.py archive + +Manual usage: + TASK_JSON_PATH=.trellis/tasks/<name>/task.json python3 .trellis/scripts/hooks/linear_sync.py sync + +Environment: + TASK_JSON_PATH - Absolute path to task.json (set by task.py) + +Configuration: + .trellis/hooks.local.json - Local config (gitignored), example: + { + "linear": { + "team": "TEAM_KEY", + "project": "Project Name", + "assignees": { + "dev-name": "linear-user-id" + } + } + } +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +# ─── Configuration ──────────────────────────────────────────────────────────── + +# Trellis priority → Linear priority (1=Urgent, 2=High, 3=Medium, 4=Low) +PRIORITY_MAP = {"P0": 1, "P1": 2, "P2": 3, "P3": 4} + +# Linear status names (must match your team's workflow) +STATUS_IN_PROGRESS = "In Progress" +STATUS_DONE = "Done" + + +def _load_config() -> dict: + """Load local hook config from .trellis/hooks.local.json.""" + task_json_path = os.environ.get("TASK_JSON_PATH", "") + if task_json_path: + # Walk up from task.json to find .trellis/ + trellis_dir = Path(task_json_path).parent.parent.parent + else: + trellis_dir = Path(".trellis") + + config_path = trellis_dir / "hooks.local.json" + try: + with open(config_path, encoding="utf-8") as f: + return json.load(f) + except (OSError, json.JSONDecodeError): + return {} + + +CONFIG = _load_config() +LINEAR_CFG = CONFIG.get("linear", {}) + +TEAM = LINEAR_CFG.get("team", "") +PROJECT = LINEAR_CFG.get("project", "") +ASSIGNEE_MAP = LINEAR_CFG.get("assignees", {}) + +# ─── Helpers ────────────────────────────────────────────────────────────────── + + +def _read_task() -> tuple[dict, str]: + path = os.environ.get("TASK_JSON_PATH", "") + if not path: + print("TASK_JSON_PATH not set", file=sys.stderr) + sys.exit(1) + with open(path, encoding="utf-8") as f: + return json.load(f), path + + +def _write_task(data: dict, path: str) -> None: + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + + +def _linearis(*args: str) -> dict | None: + result = subprocess.run( + ["linearis", *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + if result.returncode != 0: + print(f"linearis error: {result.stderr.strip()}", file=sys.stderr) + sys.exit(1) + stdout = result.stdout.strip() + if stdout: + return json.loads(stdout) + return None + + +def _get_linear_issue(task: dict) -> str | None: + meta = task.get("meta") + if isinstance(meta, dict): + return meta.get("linear_issue") + return None + + +# ─── Actions ────────────────────────────────────────────────────────────────── + + +def cmd_create() -> None: + if not TEAM: + print("No linear.team configured in hooks.local.json", file=sys.stderr) + sys.exit(1) + + task, path = _read_task() + + # Skip if already linked + if _get_linear_issue(task): + print(f"Already linked: {_get_linear_issue(task)}") + return + + title = task.get("title") or task.get("name") or "Untitled" + args = ["issues", "create", title, "--team", TEAM] + + # Map priority + priority = PRIORITY_MAP.get(task.get("priority", ""), 0) + if priority: + args.extend(["-p", str(priority)]) + + # Set project + if PROJECT: + args.extend(["--project", PROJECT]) + + # Assign to Linear user + assignee = task.get("assignee", "") + linear_user_id = ASSIGNEE_MAP.get(assignee) + if linear_user_id: + args.extend(["--assignee", linear_user_id]) + + # Link to parent's Linear issue if available + parent_issue = _resolve_parent_linear_issue(task) + if parent_issue: + args.extend(["--parent-ticket", parent_issue]) + + result = _linearis(*args) + if result and "identifier" in result: + if not isinstance(task.get("meta"), dict): + task["meta"] = {} + task["meta"]["linear_issue"] = result["identifier"] + _write_task(task, path) + print(f"Created Linear issue: {result['identifier']}") + + +def cmd_start() -> None: + task, _ = _read_task() + issue = _get_linear_issue(task) + if not issue: + return + _linearis("issues", "update", issue, "-s", STATUS_IN_PROGRESS) + print(f"Updated {issue} -> {STATUS_IN_PROGRESS}") + cmd_sync() + + +def cmd_archive() -> None: + task, _ = _read_task() + issue = _get_linear_issue(task) + if not issue: + return + _linearis("issues", "update", issue, "-s", STATUS_DONE) + print(f"Updated {issue} -> {STATUS_DONE}") + + +def cmd_sync() -> None: + """Sync prd.md content to Linear issue description.""" + task, _ = _read_task() + issue = _get_linear_issue(task) + if not issue: + print("No linear_issue in meta, run create first", file=sys.stderr) + sys.exit(1) + + # Find prd.md next to task.json + task_json_path = os.environ.get("TASK_JSON_PATH", "") + prd_path = Path(task_json_path).parent / "prd.md" + if not prd_path.is_file(): + print(f"No prd.md found at {prd_path}", file=sys.stderr) + sys.exit(1) + + description = prd_path.read_text(encoding="utf-8").strip() + _linearis("issues", "update", issue, "-d", description) + print(f"Synced prd.md to {issue} description") + + +# ─── Parent Issue Resolution ───────────────────────────────────────────────── + + +def _resolve_parent_linear_issue(task: dict) -> str | None: + """Find parent task's Linear issue identifier.""" + parent_name = task.get("parent") + if not parent_name: + return None + + task_json_path = os.environ.get("TASK_JSON_PATH", "") + if not task_json_path: + return None + + current_task_dir = Path(task_json_path).parent + tasks_dir = current_task_dir.parent + parent_json = tasks_dir / parent_name / "task.json" + + if parent_json.exists(): + try: + with open(parent_json, encoding="utf-8") as f: + parent_task = json.load(f) + return _get_linear_issue(parent_task) + except (json.JSONDecodeError, OSError): + pass + return None + + +# ─── Main ───────────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "" + actions = { + "create": cmd_create, + "start": cmd_start, + "archive": cmd_archive, + "sync": cmd_sync, + } + fn = actions.get(action) + if fn: + fn() + else: + print(f"Unknown action: {action}", file=sys.stderr) + print(f"Valid actions: {', '.join(actions)}", file=sys.stderr) + sys.exit(1) diff --git a/.trellis/scripts/init_developer.py b/.trellis/scripts/init_developer.py new file mode 100755 index 0000000..9fb53f5 --- /dev/null +++ b/.trellis/scripts/init_developer.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +Initialize developer for workflow. + +Usage: + python3 init_developer.py <developer-name> + +This creates: + - .trellis/.developer file with developer info + - .trellis/workspace/<name>/ directory structure +""" + +from __future__ import annotations + +import sys + +from common.paths import ( + DIR_WORKFLOW, + FILE_DEVELOPER, + get_developer, +) +from common.developer import init_developer + + +def main() -> None: + """CLI entry point.""" + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} <developer-name>") + print() + print("Example:") + print(f" {sys.argv[0]} john") + sys.exit(1) + + name = sys.argv[1] + + # Check if already initialized + existing = get_developer() + if existing: + print(f"Developer already initialized: {existing}") + print() + print(f"To reinitialize, remove {DIR_WORKFLOW}/{FILE_DEVELOPER} first") + sys.exit(0) + + if init_developer(name): + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.trellis/scripts/task.py b/.trellis/scripts/task.py new file mode 100755 index 0000000..6e3ef61 --- /dev/null +++ b/.trellis/scripts/task.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +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 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 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 archive <task-dir> # Archive completed task + python3 task.py list # List active tasks + python3 task.py list-archive [month] # List archived tasks + python3 task.py add-subtask <parent-dir> <child-dir> # Link child to parent + python3 task.py remove-subtask <parent-dir> <child-dir> # Unlink child from parent +""" + +from __future__ import annotations + +import argparse +import sys + +from common.log import Colors, colored +from common.paths import ( + DIR_WORKFLOW, + DIR_TASKS, + FILE_TASK_JSON, + get_repo_root, + get_developer, + get_tasks_dir, + get_current_task, +) +from common.active_task import ( + clear_active_task, + resolve_active_task, + resolve_context_key, + set_active_task, +) +from common.io import read_json, write_json +from common.task_utils import resolve_task_dir, run_task_hooks +from common.tasks import iter_active_tasks, children_progress + +# Import command handlers from split modules (also re-exports for plan.py compatibility) +from common.task_store import ( + cmd_create, + cmd_archive, + cmd_set_branch, + cmd_set_base_branch, + cmd_set_scope, + cmd_add_subtask, + cmd_remove_subtask, +) +from common.task_context import ( + cmd_add_context, + cmd_validate, + cmd_list_context, +) + + +# ============================================================================= +# Command: start / finish +# ============================================================================= + +def cmd_start(args: argparse.Namespace) -> int: + """Set active task.""" + repo_root = get_repo_root() + task_input = args.dir + + if not task_input: + print(colored("Error: task directory or name required", Colors.RED)) + return 1 + + # Resolve task directory (supports task name, relative path, or absolute path) + full_path = resolve_task_dir(task_input, repo_root) + + if not full_path.is_dir(): + print(colored(f"Error: Task not found: {task_input}", Colors.RED)) + print("Hint: Use task name (e.g., 'my-task') or full path (e.g., '.trellis/tasks/01-31-my-task')") + return 1 + + # Convert to relative path for storage + try: + task_dir = full_path.relative_to(repo_root).as_posix() + except ValueError: + task_dir = str(full_path) + + task_json_path = full_path / FILE_TASK_JSON + + if not resolve_context_key(): + # Degraded mode: no session identity available. + # Hook didn't inject TRELLIS_CONTEXT_ID (common on Windows + Claude Code, + # --continue resume path, fork distribution, hooks disabled, etc.). Skip + # per-session pointer write; AI continues based on conversation context. + print(colored( + "ℹ Session identity not available; active-task pointer not persisted " + "this session (degraded mode). AI continues based on conversation context.", + Colors.YELLOW, + )) + print(colored( + "Hint: run inside an AI IDE/session that exposes session identity, " + "or set TRELLIS_CONTEXT_ID before running task.py start.", + Colors.YELLOW, + )) + + # Still flip task.json status: planning → in_progress so downstream phases proceed. + if task_json_path.is_file(): + data = read_json(task_json_path) + if data and data.get("status") == "planning": + data["status"] = "in_progress" + if write_json(task_json_path, data): + print(colored("✓ Status: planning → in_progress (degraded)", Colors.GREEN)) + run_task_hooks("after_start", task_json_path, repo_root) + return 0 + + active = set_active_task(task_dir, repo_root) + if active: + print(colored(f"✓ Current task set to: {task_dir}", Colors.GREEN)) + print(f"Source: {active.source}") + + if task_json_path.is_file(): + data = read_json(task_json_path) + if data and data.get("status") == "planning": + data["status"] = "in_progress" + if write_json(task_json_path, data): + print(colored("✓ Status: planning → in_progress", Colors.GREEN)) + + print() + print(colored("The hook will now inject context from this task's jsonl files.", Colors.BLUE)) + + run_task_hooks("after_start", task_json_path, repo_root) + return 0 + else: + print(colored("Error: Failed to set current task", Colors.RED)) + return 1 + + +def cmd_finish(args: argparse.Namespace) -> int: + """Clear active task.""" + repo_root = get_repo_root() + active = clear_active_task(repo_root) + current = active.task_path + + if not current: + print(colored("No current task set", Colors.YELLOW)) + return 0 + + # Resolve task.json path before clearing + task_json_path = repo_root / current / FILE_TASK_JSON + + print(colored(f"✓ Cleared current task (was: {current})", Colors.GREEN)) + print(f"Source: {active.source}") + + if task_json_path.is_file(): + run_task_hooks("after_finish", task_json_path, repo_root) + return 0 + + +def cmd_current(args: argparse.Namespace) -> int: + """Show active task.""" + repo_root = get_repo_root() + active = resolve_active_task(repo_root) + + if args.source: + print(f"Current task: {active.task_path or '(none)'}") + print(f"Source: {active.source}") + if active.stale: + print("State: stale") + return 0 if active.task_path else 1 + + if active.task_path: + print(active.task_path) + return 0 + + return 1 + + +# ============================================================================= +# Command: list +# ============================================================================= + +def cmd_list(args: argparse.Namespace) -> int: + """List active tasks.""" + repo_root = get_repo_root() + tasks_dir = get_tasks_dir(repo_root) + current_task = get_current_task(repo_root) + developer = get_developer(repo_root) + filter_mine = args.mine + filter_status = args.status + + if filter_mine: + if not developer: + print(colored("Error: No developer set. Run init_developer.py first", Colors.RED), file=sys.stderr) + return 1 + print(colored(f"My tasks (assignee: {developer}):", Colors.BLUE)) + else: + print(colored("All active tasks:", Colors.BLUE)) + print() + + # Single pass: collect all tasks via shared iterator + all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)} + all_statuses = {name: t.status for name, t in all_tasks.items()} + + # Display tasks hierarchically + count = 0 + + def _print_task(dir_name: str, indent: int = 0) -> None: + nonlocal count + t = all_tasks[dir_name] + + # Apply --mine filter + if filter_mine and (t.assignee or "-") != developer: + return + + # Apply --status filter + if filter_status and t.status != filter_status: + return + + relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}" + marker = "" + if relative_path == current_task: + marker = f" {colored('<- current', Colors.GREEN)}" + + # Children progress + progress = children_progress(t.children, all_statuses) + + # Package tag + pkg_tag = f" @{t.package}" if t.package else "" + + prefix = " " * indent + " - " + + if filter_mine: + print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress}{marker}") + else: + print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}") + count += 1 + + # Print children indented + for child_name in t.children: + if child_name in all_tasks: + _print_task(child_name, indent + 1) + + # Display only top-level tasks (those without a parent) + for dir_name in sorted(all_tasks.keys()): + if not all_tasks[dir_name].parent: + _print_task(dir_name) + + if count == 0: + if filter_mine: + print(" (no tasks assigned to you)") + else: + print(" (no active tasks)") + + print() + print(f"Total: {count} task(s)") + return 0 + + +# ============================================================================= +# Command: list-archive +# ============================================================================= + +def cmd_list_archive(args: argparse.Namespace) -> int: + """List archived tasks.""" + repo_root = get_repo_root() + tasks_dir = get_tasks_dir(repo_root) + archive_dir = tasks_dir / "archive" + month = args.month + + print(colored("Archived tasks:", Colors.BLUE)) + print() + + if month: + month_dir = archive_dir / month + if month_dir.is_dir(): + print(f"[{month}]") + for d in sorted(month_dir.iterdir()): + if d.is_dir(): + print(f" - {d.name}/") + else: + print(f" No archives for {month}") + else: + if archive_dir.is_dir(): + for month_dir in sorted(archive_dir.iterdir()): + if month_dir.is_dir(): + month_name = month_dir.name + count = sum(1 for d in month_dir.iterdir() if d.is_dir()) + print(f"[{month_name}] - {count} task(s)") + + return 0 + + +# ============================================================================= +# Help +# ============================================================================= + +def show_usage() -> None: + """Show usage help.""" + print("""Task Management Script + +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 add-context <dir> <jsonl> <path> [reason] Add entry to jsonl + python3 task.py validate <dir> Validate jsonl files + python3 task.py list-context <dir> List jsonl entries + python3 task.py start <dir> Set active task + python3 task.py current [--source] 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 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-archive [YYYY-MM] List archived tasks + +Monorepo options: + --package <pkg> Package name (validated against config.yaml packages) + +List options: + --mine, -m Show only tasks assigned to current developer + --status, -s <s> Filter by status (planning, in_progress, review, completed) + +Examples: + python3 task.py create "Add login feature" --slug add-login + python3 task.py create "Add login feature" --slug add-login --package cli + python3 task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent + python3 task.py add-context <dir> implement .trellis/spec/cli/backend/auth.md "Auth guidelines" + python3 task.py set-branch <dir> task/add-login + python3 task.py start .trellis/tasks/01-21-add-login + python3 task.py current --source + python3 task.py finish + python3 task.py archive add-login + python3 task.py add-subtask parent-task child-task # Link existing tasks + python3 task.py remove-subtask parent-task child-task + python3 task.py list # List all active tasks + python3 task.py list --mine # List my tasks only + python3 task.py list --mine --status in_progress # List my in-progress tasks +""") + + +# ============================================================================= +# Main Entry +# ============================================================================= + +def main() -> int: + """CLI entry point.""" + # Deprecation guard: `init-context` was removed in v0.5.0-beta.12. + # Detect early so argparse doesn't mask the real reason with a generic + # "invalid choice" error. + if len(sys.argv) >= 2 and sys.argv[1] == "init-context": + print( + colored( + "Error: `task.py init-context` was removed in v0.5.0-beta.12.", + Colors.RED, + ), + file=sys.stderr, + ) + print( + "implement.jsonl / check.jsonl are now seeded on `task.py create` for", + file=sys.stderr, + ) + print( + "sub-agent-capable platforms and curated by the AI during planning when needed.", + 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", + file=sys.stderr, + ) + print( + "Use `task.py add-context <dir> implement|check <path> <reason>` to append entries.", + file=sys.stderr, + ) + return 2 + + parser = argparse.ArgumentParser( + description="Task Management Script", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + subparsers = parser.add_subparsers(dest="command", help="Commands") + + # create + p_create = subparsers.add_parser("create", help="Create new task") + p_create.add_argument("title", help="Task title") + p_create.add_argument("--slug", "-s", help="Task slug") + p_create.add_argument("--assignee", "-a", help="Assignee developer") + p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)") + p_create.add_argument("--description", "-d", help="Task description") + p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)") + p_create.add_argument("--package", help="Package name for monorepo projects") + + # add-context + p_add = subparsers.add_parser("add-context", help="Add context entry") + p_add.add_argument("dir", help="Task directory") + p_add.add_argument("file", help="JSONL file (implement|check)") + p_add.add_argument("path", help="File path to add") + p_add.add_argument("reason", nargs="?", help="Reason for adding") + + # validate + p_validate = subparsers.add_parser("validate", help="Validate context files") + p_validate.add_argument("dir", help="Task directory") + + # list-context + p_listctx = subparsers.add_parser("list-context", help="List context entries") + p_listctx.add_argument("dir", help="Task directory") + + # start + p_start = subparsers.add_parser("start", help="Set active task") + p_start.add_argument("dir", help="Task directory") + + # current + p_current = subparsers.add_parser("current", help="Show active task") + p_current.add_argument("--source", action="store_true", + help="Show active task source") + + # finish + subparsers.add_parser("finish", help="Clear active task") + + # set-branch + p_branch = subparsers.add_parser("set-branch", help="Set git branch") + p_branch.add_argument("dir", help="Task directory") + p_branch.add_argument("branch", help="Branch name") + + # set-base-branch + p_base = subparsers.add_parser("set-base-branch", help="Set PR target branch") + p_base.add_argument("dir", help="Task directory") + p_base.add_argument("base_branch", help="Base branch name (PR target)") + + # set-scope + p_scope = subparsers.add_parser("set-scope", help="Set scope") + p_scope.add_argument("dir", help="Task directory") + p_scope.add_argument("scope", help="Scope name") + + # archive + p_archive = subparsers.add_parser("archive", help="Archive task") + p_archive.add_argument("name", help="Task directory or name") + p_archive.add_argument("--no-commit", action="store_true", help="Skip auto git commit after archive") + + # list + p_list = subparsers.add_parser("list", help="List tasks") + p_list.add_argument("--mine", "-m", action="store_true", help="My tasks only") + p_list.add_argument("--status", "-s", help="Filter by status") + + # add-subtask + p_addsub = subparsers.add_parser("add-subtask", help="Link child task to parent") + p_addsub.add_argument("parent_dir", help="Parent task directory") + p_addsub.add_argument("child_dir", help="Child task directory") + + # remove-subtask + p_rmsub = subparsers.add_parser("remove-subtask", help="Unlink child task from parent") + p_rmsub.add_argument("parent_dir", help="Parent task directory") + p_rmsub.add_argument("child_dir", help="Child task directory") + + # list-archive + p_listarch = subparsers.add_parser("list-archive", help="List archived tasks") + p_listarch.add_argument("month", nargs="?", help="Month (YYYY-MM)") + + args = parser.parse_args() + + if not args.command: + show_usage() + return 1 + + commands = { + "create": cmd_create, + "add-context": cmd_add_context, + "validate": cmd_validate, + "list-context": cmd_list_context, + "start": cmd_start, + "current": cmd_current, + "finish": cmd_finish, + "set-branch": cmd_set_branch, + "set-base-branch": cmd_set_base_branch, + "set-scope": cmd_set_scope, + "archive": cmd_archive, + "add-subtask": cmd_add_subtask, + "remove-subtask": cmd_remove_subtask, + "list": cmd_list, + "list-archive": cmd_list_archive, + } + + if args.command in commands: + return commands[args.command](args) + else: + show_usage() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.trellis/spec/README.md b/.trellis/spec/README.md new file mode 100644 index 0000000..c2c995c --- /dev/null +++ b/.trellis/spec/README.md @@ -0,0 +1,77 @@ +# Next.js Full-Stack Development Guidelines + +Universal development guidelines for production Next.js applications with oRPC API layer and PostgreSQL. + +## Structure + +### [Frontend](./frontend/index.md) + +React 19 + Next.js 15 App Router frontend development patterns: + +- [Directory Structure](./frontend/directory-structure.md) +- [Components](./frontend/components.md) +- [State Management](./frontend/state-management.md) +- [Hooks](./frontend/hooks.md) +- [API Integration](./frontend/api-integration.md) +- [oRPC Usage](./frontend/orpc-usage.md) +- [Authentication](./frontend/authentication.md) +- [AI SDK Integration](./frontend/ai-sdk-integration.md) +- [CSS & Layout](./frontend/css-layout.md) +- [Type Safety](./frontend/type-safety.md) +- [Quality Checklist](./frontend/quality.md) + +### [Backend](./backend/index.md) + +oRPC + Drizzle ORM backend development patterns: + +- [Directory Structure](./backend/directory-structure.md) +- [oRPC Usage](./backend/orpc-usage.md) +- [Authentication](./backend/authentication.md) +- [Database](./backend/database.md) +- [AI SDK Integration](./backend/ai-sdk-integration.md) +- [Logging](./backend/logging.md) +- [Performance](./backend/performance.md) +- [Type Safety](./backend/type-safety.md) +- [Quality Checklist](./backend/quality.md) + +### [Shared](./shared/index.md) + +Cross-cutting concerns: + +- [Dependencies](./shared/dependencies.md) +- [Code Quality](./shared/code-quality.md) +- [TypeScript Conventions](./shared/typescript.md) + +### [Guides](./guides/index.md) + +Development thinking guides: + +- [Pre-Implementation Checklist](./guides/pre-implementation-checklist.md) +- [Cross-Layer Thinking Guide](./guides/cross-layer-thinking-guide.md) + +### [Common Issues / Pitfalls](./big-question/index.md) + +Common issues and solutions: + +- [PostgreSQL JSON vs JSONB](./big-question/postgres-json-jsonb.md) +- [WebKit Tap Highlight](./big-question/webkit-tap-highlight.md) +- [Sentry & next-intl Conflict](./big-question/sentry-nextintl-conflict.md) +- [Turbopack vs Webpack Flexbox](./big-question/turbopack-webpack-flexbox.md) + +## Tech Stack + +- **Frontend**: Next.js 15, React 19, TailwindCSS 4, Radix UI, React Query +- **Backend**: oRPC, Drizzle ORM, PostgreSQL, better-auth +- **AI**: Vercel AI SDK (multi-provider) +- **Real-time**: Ably / WebSocket / SSE +- **Build**: Turborepo + pnpm (monorepo) +- **Monitoring**: Sentry + OpenTelemetry + +## Usage + +These guidelines can be used as: + +1. **New Project Template** - Copy the entire structure for new Next.js projects +2. **Reference Documentation** - Consult specific guides when implementing features +3. **Code Review Checklist** - Verify implementations against established patterns +4. **Onboarding Material** - Help new developers understand project conventions diff --git a/.trellis/spec/backend/ai-sdk-integration.md b/.trellis/spec/backend/ai-sdk-integration.md new file mode 100644 index 0000000..878e1a1 --- /dev/null +++ b/.trellis/spec/backend/ai-sdk-integration.md @@ -0,0 +1,351 @@ +# AI SDK Backend Integration Guidelines + +## 1. Overview + +This document covers backend integration patterns using the Vercel AI SDK (`ai` package) for AI-powered features. + +### Supported Providers +- **OpenAI**: GPT-4o, GPT-4o-mini, GPT-4-turbo +- **Google Gemini**: gemini-1.5-pro, gemini-1.5-flash +- **Anthropic**: Claude 3.5 Sonnet, Claude 3 Opus + +### Package Dependencies +```bash +pnpm add ai @ai-sdk/openai @ai-sdk/google @ai-sdk/anthropic +``` + +## 2. Basic Usage + +### generateText + +Use `generateText` for simple text generation tasks where you need a complete response. + +```typescript +import { generateText } from "ai"; +import { openai } from "@ai-sdk/openai"; + +const { text } = await generateText({ + model: openai("gpt-4o-mini"), + prompt: "Summarize this document...", +}); +``` + +### generateObject (Structured Output with Zod) + +Use `generateObject` when you need type-safe structured output. The AI SDK validates the response against your Zod schema automatically. + +```typescript +import { generateObject } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { z } from "zod"; + +const classificationSchema = z.object({ + category: z.enum(["urgent", "normal", "low"]), + confidence: z.number().min(0).max(1), + reasoning: z.string(), +}); + +const { object } = await generateObject({ + model: openai("gpt-4o-mini"), + schema: classificationSchema, + prompt: "Classify the priority of this task...", +}); +// object is typed as { category: "urgent" | "normal" | "low", confidence: number, reasoning: string } +``` + +### streamText (For SSE/Streaming) + +Use `streamText` for real-time streaming responses, ideal for chat interfaces and long-form content generation. + +```typescript +import { streamText } from "ai"; +import { openai } from "@ai-sdk/openai"; + +const result = streamText({ + model: openai("gpt-4o"), + messages: conversationHistory, + system: "You are a helpful assistant.", +}); + +// Return as SSE stream +return result.toDataStreamResponse(); +``` + +## 3. Telemetry Configuration + +**IMPORTANT**: Always enable telemetry for token tracking and performance monitoring. + +```typescript +import { generateObject } from "ai"; +import { openai } from "@ai-sdk/openai"; + +const { object } = await generateObject({ + model: openai("gpt-4o-mini"), + schema: mySchema, + prompt, + experimental_telemetry: { + isEnabled: true, + functionId: "orders.classify", // Module.function naming + metadata: { + orderId, + userId, + }, + }, +}); +``` + +### Telemetry Naming Convention + +Use dot-separated format for `functionId`: `module.function` + +| Module | Example functionId | +|--------|-------------------| +| Orders | `orders.classify`, `orders.summarize` | +| Support | `support.generateReply`, `support.categorize` | +| Content | `content.summarize`, `content.translate` | +| Users | `users.analyzePreferences` | + +### Auto-recorded Metrics + +When telemetry is enabled, these metrics are automatically tracked: + +| Metric | Description | +|--------|-------------| +| `ai.model.id` | Model identifier (e.g., gpt-4o-mini) | +| `ai.model.provider` | Provider name (e.g., openai) | +| `ai.usage.prompt_tokens` | Input tokens consumed | +| `ai.usage.completion_tokens` | Output tokens generated | +| `ai.usage.total_tokens` | Total tokens used | +| `ai.response.finish_reason` | Completion reason (stop, length, etc.) | + +## 4. Tool Calling + +Define tools that the AI model can invoke to perform actions in your system. + +```typescript +import { generateText, tool } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { z } from "zod"; + +const result = await generateText({ + model: openai("gpt-4o"), + prompt: "Create a task for the user...", + tools: { + createTask: tool({ + description: "Create a new task in the system", + parameters: z.object({ + title: z.string(), + dueDate: z.string().optional(), + priority: z.enum(["high", "medium", "low"]), + }), + execute: async ({ title, dueDate, priority }) => { + const task = await db.insert(tasks).values({ + title, + dueDate: dueDate ? new Date(dueDate) : null, + priority, + }).returning(); + return { success: true, taskId: task[0].id }; + }, + }), + searchOrders: tool({ + description: "Search for orders by criteria", + parameters: z.object({ + query: z.string(), + status: z.enum(["pending", "completed", "cancelled"]).optional(), + limit: z.number().default(10), + }), + execute: async ({ query, status, limit }) => { + const orders = await db.query.orders.findMany({ + where: and( + like(orders.title, `%${query}%`), + status ? eq(orders.status, status) : undefined + ), + limit, + }); + return { orders }; + }, + }), + }, +}); + +// Access tool results +if (result.toolCalls) { + for (const toolCall of result.toolCalls) { + console.log(`Tool: ${toolCall.toolName}`, toolCall.result); + } +} +``` + +## 5. Error Handling + +Always implement graceful error handling for AI operations. + +```typescript +import { generateObject } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { logger } from "@your-app/logs"; + +async function classifyOrder(orderData: OrderData) { + try { + const { object } = await generateObject({ + model: openai("gpt-4o-mini"), + schema: classificationSchema, + prompt: buildClassificationPrompt(orderData), + experimental_telemetry: { + isEnabled: true, + functionId: "orders.classify", + }, + }); + return { success: true, data: object }; + } catch (error) { + logger.error("AI generation failed", { + error, + orderId: orderData.id, + prompt: buildClassificationPrompt(orderData).slice(0, 100) + }); + + // Return graceful fallback + return { + success: false, + reason: "AI processing failed", + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} +``` + +### Common Error Types + +| Error | Cause | Resolution | +|-------|-------|------------| +| Rate limit exceeded | Too many requests | Implement exponential backoff | +| Context length exceeded | Prompt too long | Truncate or summarize input | +| Invalid API key | Missing/wrong credentials | Check environment variables | +| Schema validation failed | AI output doesn't match schema | Adjust schema or prompt | + +## 6. Prompt Engineering Best Practices + +### Use XML Structure for Complex Prompts + +XML tags help the AI model better understand the structure of your request. + +```typescript +const prompt = ` +<context> +${contextData} +</context> + +<task> +Analyze the above context and extract key information. +</task> + +<output_format> +Return a JSON object with the following fields: +- summary: A brief summary +- keyPoints: Array of key points +- sentiment: positive, negative, or neutral +</output_format> +`; +``` + +### System Prompts + +Define consistent behavior with system prompts. + +```typescript +import { generateText } from "ai"; +import { openai } from "@ai-sdk/openai"; + +const result = await generateText({ + model: openai("gpt-4o"), + system: `You are a professional assistant. +Always respond in a structured format. +Be concise and accurate. +Never make up information - if unsure, say so.`, + messages: userMessages, +}); +``` + +### Multi-step Prompts + +For complex tasks, break down into multiple AI calls. + +```typescript +// Step 1: Extract entities +const { object: entities } = await generateObject({ + model: openai("gpt-4o-mini"), + schema: entitiesSchema, + prompt: `Extract entities from: ${document}`, +}); + +// Step 2: Classify based on entities +const { object: classification } = await generateObject({ + model: openai("gpt-4o-mini"), + schema: classificationSchema, + prompt: ` +<entities> +${JSON.stringify(entities, null, 2)} +</entities> + +<task> +Based on these entities, classify the document category. +</task> +`, +}); +``` + +## 7. Provider-Specific Configuration + +### OpenAI + +```typescript +import { openai } from "@ai-sdk/openai"; + +const model = openai("gpt-4o-mini", { + // Optional: custom configuration +}); +``` + +### Google Gemini + +```typescript +import { google } from "@ai-sdk/google"; + +const model = google("gemini-1.5-flash"); +``` + +### Anthropic + +```typescript +import { anthropic } from "@ai-sdk/anthropic"; + +const model = anthropic("claude-3-5-sonnet-20241022"); +``` + +## 8. Best Practices Summary + +| Rule | Description | +|------|-------------| +| Always enable telemetry | Track token usage and performance for cost monitoring | +| Use generateObject for structured output | Leverage Zod schemas for type safety and validation | +| Use XML prompts for complex tasks | Better structure improves AI understanding | +| Handle errors gracefully | Return fallback responses, never crash | +| Log AI failures | Include context (truncated prompt, IDs) for debugging | +| Use appropriate model sizes | Use mini models for simple tasks, larger for complex | +| Implement rate limiting | Protect against API quota exhaustion | +| Cache responses when appropriate | Reduce costs for repeated queries | + +## 9. Environment Variables + +Required environment variables for AI providers: + +```bash +# OpenAI +OPENAI_API_KEY=sk-... + +# Google Gemini +GOOGLE_GENERATIVE_AI_API_KEY=... + +# Anthropic +ANTHROPIC_API_KEY=sk-ant-... +``` diff --git a/.trellis/spec/backend/authentication.md b/.trellis/spec/backend/authentication.md new file mode 100644 index 0000000..b08247e --- /dev/null +++ b/.trellis/spec/backend/authentication.md @@ -0,0 +1,723 @@ +# Authentication Guidelines + +This document covers backend authentication integration using better-auth, including session management, protected procedures, and OAuth configuration. + +## 1. Overview + +### What is better-auth + +better-auth is a modern authentication library for TypeScript applications that provides: + +- Session-based authentication with secure cookie management +- Multiple authentication methods (email/password, OAuth, magic links, passkeys) +- Built-in support for organizations and multi-tenancy +- Database adapter integration (Drizzle ORM) +- Two-factor authentication (2FA) +- Admin functionality + +### Session-based Authentication + +The authentication system uses secure, HTTP-only cookies to manage user sessions: + +- Sessions are stored in the database and cached in Redis for performance +- Session tokens are automatically validated on each request +- Cookie names are prefixed with `__Secure-` in production (HTTPS) + +### Supported Providers + +| Provider | Type | Description | +|----------|------|-------------| +| Email/Password | Credential | Traditional email and password authentication | +| Google | OAuth | Social login with Google account | +| GitHub | OAuth | Social login with GitHub account | +| Magic Link | Passwordless | Email-based one-time login links | +| Passkey | Passwordless | WebAuthn/FIDO2 biometric authentication | + +## 2. Auth Configuration + +### Server-side Auth Setup + +The auth configuration is defined in the auth package: + +```typescript +// packages/auth/auth.ts +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { db } from "@your-app/database"; +import { + admin, + magicLink, + organization, + passkey, + twoFactor, + username, +} from "better-auth/plugins"; + +export const auth = betterAuth({ + baseURL: process.env.APP_URL, + appName: "Your App Name", + + // Database adapter + database: drizzleAdapter(db, { + provider: "pg", + }), + + // Session configuration + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days in seconds + freshAge: 0, + }, + + // Account linking for OAuth providers + account: { + accountLinking: { + enabled: true, + trustedProviders: ["google", "github"], + }, + }, + + // Plugins + plugins: [ + username(), + admin(), + passkey(), + magicLink({ + sendMagicLink: async ({ email, url }, request) => { + // Send magic link email + await sendEmail({ + to: email, + templateId: "magicLink", + context: { url }, + }); + }, + }), + organization({ + sendInvitationEmail: async ({ email, id, organization }, request) => { + // Send organization invitation email + }, + }), + twoFactor(), + ], +}); + +// Export session type +export type Session = typeof auth.$Infer.Session; +``` + +### Database Adapter (Drizzle) + +better-auth uses Drizzle ORM for database operations. The required tables are automatically created: + +- `user` - User accounts +- `session` - Authentication sessions +- `account` - OAuth provider accounts (Google, GitHub, etc.) +- `verification` - Email verification tokens + +### Session Configuration + +```typescript +session: { + // Session lifetime (default: 7 days) + expiresIn: 60 * 60 * 24 * 7, + + // Fresh session age for sensitive operations (0 = always require re-auth) + freshAge: 0, +} +``` + +## 3. Protected Procedures + +### Procedure Types + +The API layer provides three procedure types with different authentication levels: + +```typescript +// packages/api/orpc/procedures.ts +import { ORPCError, os } from "@orpc/server"; + +// Public procedure - no authentication required +export const publicProcedure = os + .$context<{ headers: Headers }>() + .use(logIdMiddleware); + +// Protected procedure - requires authenticated user +export const protectedProcedure = publicProcedure.use( + async ({ context, next }) => { + const { session } = await getSessionWithCache(context.headers); + + if (!session) { + throw new ORPCError("UNAUTHORIZED"); + } + + return await next({ + context: { + session: session.session, + user: session.user, + }, + }); + }, +); + +// Admin procedure - requires admin role +export const adminProcedure = protectedProcedure.use( + async ({ context, next }) => { + if (context.user.role !== "admin") { + throw new ORPCError("FORBIDDEN"); + } + + return await next(); + }, +); +``` + +### Using Protected Procedures + +**Basic protected endpoint:** + +```typescript +// procedures/get-profile.ts +import { protectedProcedure } from "../../../orpc/procedures"; + +export const getProfile = protectedProcedure + .route({ + method: "GET", + path: "/users/profile", + tags: ["Users"], + summary: "Get current user profile", + }) + .handler(async ({ context }) => { + // Access authenticated user from context + const { user, session } = context; + + return { + success: true, + reason: "Profile retrieved", + user: { + id: user.id, + email: user.email, + name: user.name, + }, + }; + }); +``` + +**Admin-only endpoint:** + +```typescript +// procedures/list-users.ts +import { adminProcedure } from "../../../orpc/procedures"; +import { z } from "zod"; + +export const listUsers = adminProcedure + .route({ + method: "GET", + path: "/admin/users", + tags: ["Administration"], + summary: "List all users", + }) + .input( + z.object({ + limit: z.number().min(1).max(100).default(10), + offset: z.number().min(0).default(0), + }), + ) + .handler(async ({ input: { limit, offset } }) => { + const users = await getUsers({ limit, offset }); + return { users }; + }); +``` + +### Accessing User Session in Context + +The protected procedure middleware injects session data into the context: + +```typescript +interface ProtectedContext { + session: { + id: string; + userId: string; + expiresAt: Date; + // ... other session fields + }; + user: { + id: string; + email: string; + name: string; + role: "user" | "admin"; + // ... other user fields + }; +} +``` + +**Accessing context in handlers:** + +```typescript +.handler(async ({ context, input }) => { + const { user, session } = context; + + // Use user.id for database queries + const userOrders = await getOrdersByUserId(user.id); + + // Check user role + if (user.role === "admin") { + // Admin-specific logic + } + + return { success: true, reason: "Success", orders: userOrders }; +}); +``` + +### Role-based Access Control + +**Custom role middleware:** + +```typescript +// Create a middleware for specific roles +const organizationAdminProcedure = protectedProcedure.use( + async ({ context, input, next }) => { + const { organizationId } = input as { organizationId: string }; + + const membership = await getOrganizationMembership( + organizationId, + context.user.id + ); + + if (!membership || membership.role !== "owner") { + throw new ORPCError("FORBIDDEN", { + message: "Organization admin access required", + }); + } + + return await next({ + context: { + ...context, + organization: membership.organization, + }, + }); + }, +); +``` + +**Verifying organization membership:** + +```typescript +// lib/membership.ts +import { getOrganizationMembership } from "@your-app/database"; + +export async function verifyOrganizationMembership( + organizationId: string, + userId: string, +) { + const membership = await getOrganizationMembership(organizationId, userId); + + if (!membership) { + return null; + } + + return { + organization: membership.organization, + role: membership.role, + }; +} +``` + +## 4. Session Management + +### Session Caching + +Sessions are cached in Redis to reduce database load: + +```typescript +// lib/session-cache.ts +import { auth } from "@your-app/auth"; +import { redis } from "./redis"; + +const SESSION_CACHE_PREFIX = "session"; +const SESSION_TTL = 60 * 60 * 24 * 7; // 7 days + +export async function getSessionWithCache( + headers: Headers, +): Promise<{ session: Session | null; fromCache: boolean }> { + const sessionToken = getSessionTokenFromHeaders(headers); + + if (!sessionToken) { + const fresh = await fetchSession(headers); + return { session: fresh, fromCache: false }; + } + + // Try cache first + const cached = await redis.get(`${SESSION_CACHE_PREFIX}:${sessionToken}`); + if (cached) { + return { session: JSON.parse(cached), fromCache: true }; + } + + // Fetch from database + const fresh = await auth.api.getSession({ headers }); + + if (fresh) { + // Cache the session + await redis.set( + `${SESSION_CACHE_PREFIX}:${sessionToken}`, + JSON.stringify(fresh), + { ex: SESSION_TTL } + ); + } + + return { session: fresh, fromCache: false }; +} +``` + +### Getting Session Token from Headers + +```typescript +export function getSessionTokenFromHeaders(headers: Headers): string | null { + // Check Authorization header first + const authHeader = headers.get("Authorization"); + if (authHeader?.startsWith("Bearer ")) { + return authHeader.slice("Bearer ".length); + } + + // Fall back to cookie + const cookieHeader = headers.get("cookie"); + if (!cookieHeader) { + return null; + } + + const cookies = parseCookie(cookieHeader); + const cookieName = process.env.NODE_ENV === "production" + ? "__Secure-better-auth.session_token" + : "better-auth.session_token"; + + return cookies[cookieName] ?? null; +} +``` + +### Session Invalidation + +```typescript +// Delete session cache on logout or session change +export async function deleteSessionCache(sessionToken: string): Promise<void> { + await redis.del(`${SESSION_CACHE_PREFIX}:${sessionToken}`); +} +``` + +## 5. OAuth Integration + +### Google OAuth Setup + +**Configuration:** + +```typescript +// auth.ts +socialProviders: { + google: { + clientId: process.env.GOOGLE_CLIENT_ID as string, + clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, + scope: [ + "email", + "profile", + "openid", + // Add additional scopes as needed + // "https://www.googleapis.com/auth/calendar", + ], + // Get refresh token for offline access + accessType: "offline", + prompt: "consent", + }, +}, +``` + +**Environment variables:** + +```bash +# .env +GOOGLE_CLIENT_ID=your-google-client-id +GOOGLE_CLIENT_SECRET=your-google-client-secret +``` + +### GitHub OAuth Setup + +```typescript +socialProviders: { + github: { + clientId: process.env.GITHUB_CLIENT_ID as string, + clientSecret: process.env.GITHUB_CLIENT_SECRET as string, + scope: ["user:email"], + }, +}, +``` + +### Accessing OAuth Tokens + +To access stored OAuth tokens for API calls: + +```typescript +import { db } from "@your-app/database"; +import { account } from "@your-app/database/drizzle/schema"; +import { eq, and } from "drizzle-orm"; + +export async function getOAuthToken(userId: string, provider: string) { + const accountRecord = await db.query.account.findFirst({ + where: and( + eq(account.userId, userId), + eq(account.providerId, provider) + ), + }); + + if (!accountRecord) { + return null; + } + + return { + accessToken: accountRecord.accessToken, + refreshToken: accountRecord.refreshToken, + expiresAt: accountRecord.accessTokenExpiresAt, + }; +} +``` + +### Token Refresh + +better-auth handles token refresh automatically. For manual refresh: + +```typescript +import { auth } from "@your-app/auth"; + +export async function refreshOAuthToken(userId: string, provider: string) { + // Use auth API to refresh token + const result = await auth.api.refreshAccessToken({ + userId, + providerId: provider, + }); + + return result; +} +``` + +## 6. Error Handling + +### Standard Auth Errors + +Use oRPC error codes for authentication failures: + +```typescript +import { ORPCError } from "@orpc/server"; + +// User not authenticated +throw new ORPCError("UNAUTHORIZED"); + +// User authenticated but lacks permission +throw new ORPCError("FORBIDDEN", { + message: "Admin access required", +}); + +// Session expired +throw new ORPCError("UNAUTHORIZED", { + message: "Session expired, please login again", +}); +``` + +### Error Response Pattern + +```typescript +// Consistent error response structure +export const authErrorSchema = z.object({ + success: z.literal(false), + reason: z.string(), + code: z.enum(["UNAUTHORIZED", "FORBIDDEN", "SESSION_EXPIRED"]).optional(), +}); + +// In handler +if (!hasPermission) { + return { + success: false, + reason: "You do not have permission to perform this action", + code: "FORBIDDEN", + }; +} +``` + +### Handling Session Expiration + +```typescript +// Graceful session expiration handling +export async function handleSessionExpiration(sessionToken: string) { + // Clear cache + await deleteSessionCache(sessionToken); + + // Log the event + logger.info("Session expired", { sessionToken: sessionToken.slice(0, 10) }); + + throw new ORPCError("UNAUTHORIZED", { + message: "Your session has expired. Please login again.", + }); +} +``` + +## 7. Best Practices + +### Always Validate Session in Protected Routes + +```typescript +// GOOD - Use protectedProcedure for authenticated endpoints +export const updateProfile = protectedProcedure + .route({ method: "PATCH", path: "/users/profile" }) + .handler(async ({ context }) => { + // context.user is guaranteed to exist + }); + +// BAD - Manual session check in public procedure +export const updateProfile = publicProcedure + .handler(async ({ context }) => { + const session = await getSession(context.headers); + if (!session) throw new ORPCError("UNAUTHORIZED"); + // Error-prone and inconsistent + }); +``` + +### Use Middleware for Reusable Auth Checks + +```typescript +// Create reusable middleware for common patterns +const withOrganization = async ({ context, input, next }) => { + const { organizationId } = input; + + const membership = await verifyOrganizationMembership( + organizationId, + context.user.id + ); + + if (!membership) { + throw new ORPCError("FORBIDDEN", { + message: "Not a member of this organization", + }); + } + + return next({ + context: { ...context, organization: membership.organization }, + }); +}; + +// Use in procedures +export const getOrganizationData = protectedProcedure + .use(withOrganization) + .handler(async ({ context }) => { + // context.organization is now available + }); +``` + +### Proper Error Responses + +```typescript +// Always return meaningful error messages +.handler(async ({ context, input }) => { + try { + const result = await performAction(input); + return { success: true, reason: "Action completed", data: result }; + } catch (error) { + if (error instanceof ORPCError) { + throw error; // Re-throw oRPC errors + } + + logger.error("Action failed", { error, userId: context.user.id }); + + return { + success: false, + reason: "An unexpected error occurred", + }; + } +}); +``` + +### Secure Session Token Handling + +```typescript +// Never log full session tokens +logger.info("Session validated", { + sessionToken: `${token.substring(0, 10)}...`, + userId: session.user.id, +}); + +// Clear sensitive data from responses +const sanitizedUser = { + id: user.id, + email: user.email, + name: user.name, + // Don't include: passwordHash, sessionTokens, etc. +}; +``` + +### Cache Invalidation on Auth Events + +```typescript +// In auth hooks +hooks: { + after: createAuthMiddleware(async (ctx) => { + if (ctx.path.startsWith("/sign-out")) { + const sessionToken = getSessionTokenFromHeaders(ctx.headers); + if (sessionToken) { + await deleteSessionCache(sessionToken); + } + } + }), +} +``` + +## Client-side Auth Usage + +For client-side authentication, use the auth client: + +```typescript +// packages/auth/client.ts +import { createAuthClient } from "better-auth/react"; +import { + adminClient, + magicLinkClient, + organizationClient, + passkeyClient, + twoFactorClient, +} from "better-auth/client/plugins"; + +export const authClient = createAuthClient({ + plugins: [ + magicLinkClient(), + organizationClient(), + adminClient(), + passkeyClient(), + twoFactorClient(), + ], +}); +``` + +**Usage in React components:** + +```typescript +import { authClient } from "@your-app/auth/client"; + +// Sign in +await authClient.signIn.email({ + email: "user@example.com", + password: "password", +}); + +// Sign out +await authClient.signOut(); + +// Get current session +const session = await authClient.getSession(); + +// Use hooks +const { data: session, isPending } = authClient.useSession(); +``` + +## Quick Reference + +| Task | Solution | +|------|----------| +| Require authentication | Use `protectedProcedure` | +| Require admin role | Use `adminProcedure` | +| Get current user | Access `context.user` in handler | +| Get session data | Access `context.session` in handler | +| Check organization membership | Use `verifyOrganizationMembership` helper | +| Throw auth error | `throw new ORPCError("UNAUTHORIZED")` | +| Throw permission error | `throw new ORPCError("FORBIDDEN")` | diff --git a/.trellis/spec/backend/database.md b/.trellis/spec/backend/database.md new file mode 100644 index 0000000..c60e01c --- /dev/null +++ b/.trellis/spec/backend/database.md @@ -0,0 +1,419 @@ +# Database Operations + +This document covers database best practices using Drizzle ORM with PostgreSQL. + +## Critical Rules + +### 1. NO `await` in Loops (N+1 Problem) + +Never use `await` inside a loop. This creates the N+1 query problem, causing severe performance degradation. + +```typescript +// BAD - N+1 queries (1 query per iteration) +const orders = await db.select().from(orderTable).where(eq(orderTable.userId, userId)); +for (const order of orders) { + const items = await db.select().from(orderItemTable).where(eq(orderItemTable.orderId, order.id)); + order.items = items; +} + +// GOOD - 2 queries total using inArray +const orders = await db.select().from(orderTable).where(eq(orderTable.userId, userId)); +const orderIds = orders.map(o => o.id); + +// Single query for all items +const allItems = await db + .select() + .from(orderItemTable) + .where(inArray(orderItemTable.orderId, orderIds)); + +// Group items by orderId in memory +const itemsByOrder = new Map<string, typeof allItems>(); +for (const item of allItems) { + const existing = itemsByOrder.get(item.orderId) || []; + existing.push(item); + itemsByOrder.set(item.orderId, existing); +} + +// Attach to orders +const ordersWithItems = orders.map(order => ({ + ...order, + items: itemsByOrder.get(order.id) || [], +})); +``` + +### 2. Batch Insert Pattern + +Use batch inserts for multiple records instead of individual inserts. + +```typescript +// BAD - Multiple insert statements +for (const item of items) { + await db.insert(orderItemTable).values(item); +} + +// GOOD - Single batch insert +await db.insert(orderItemTable).values(items); + +// With returning clause +const insertedItems = await db + .insert(orderItemTable) + .values(items) + .returning(); +``` + +### 3. Conflict Handling with `onConflictDoUpdate` + +Handle upserts efficiently with conflict resolution. + +```typescript +// Upsert single record +await db + .insert(userSettingsTable) + .values({ + userId, + theme: "dark", + notifications: true, + }) + .onConflictDoUpdate({ + target: userSettingsTable.userId, + set: { + theme: sql`excluded.theme`, + notifications: sql`excluded.notifications`, + updatedAt: sql`NOW()`, + }, + }); + +// Batch upsert with composite key +const upsertedRecords = await db + .insert(inventoryTable) + .values(inventoryData) + .onConflictDoUpdate({ + target: [inventoryTable.warehouseId, inventoryTable.productId], + set: { + quantity: sql`excluded.quantity`, + updatedAt: sql`NOW()`, + }, + }) + .returning({ + id: inventoryTable.id, + productId: inventoryTable.productId, + }); +``` + +## Query Organization + +Database queries should be organized in `packages/database/drizzle/queries/`. + +``` +packages/database/drizzle/queries/ +├── index.ts # Re-exports all query modules +├── types.ts # Shared query types +├── users.ts # User-related queries +├── orders.ts # Order-related queries +└── products.ts # Product-related queries +``` + +**Example: `queries/orders.ts`** + +```typescript +import { and, desc, eq, inArray, sql } from "drizzle-orm"; +import { db } from "../client"; +import { order as orderTable, orderItem as orderItemTable } from "../schema/postgres"; + +/** + * Bulk upsert orders with conflict handling + */ +export async function bulkUpsertOrders( + ordersData: Array<{ + externalId: string; + customerId: string; + status: string; + total: number; + }>, +) { + if (ordersData.length === 0) { + return []; + } + + const upserted = await db + .insert(orderTable) + .values(ordersData) + .onConflictDoUpdate({ + target: [orderTable.externalId], + set: { + status: sql`excluded.status`, + total: sql`excluded.total`, + updatedAt: sql`NOW()`, + }, + }) + .returning({ + id: orderTable.id, + externalId: orderTable.externalId, + }); + + return upserted; +} + +/** + * Get orders with items for a user + */ +export async function getOrdersWithItems(params: { + userId: string; + limit?: number; +}) { + const { userId, limit = 20 } = params; + + const orders = await db + .select() + .from(orderTable) + .where(eq(orderTable.userId, userId)) + .orderBy(desc(orderTable.createdAt)) + .limit(limit); + + if (orders.length === 0) { + return []; + } + + const orderIds = orders.map(o => o.id); + const items = await db + .select() + .from(orderItemTable) + .where(inArray(orderItemTable.orderId, orderIds)); + + const itemsByOrder = groupBy(items, "orderId"); + + return orders.map(order => ({ + ...order, + items: itemsByOrder.get(order.id) || [], + })); +} +``` + +## Advanced SQL Patterns + +### JSON Column Operations + +When using PostgreSQL JSON/JSONB columns, proper casting is required for JSON functions. + +```typescript +// BAD - Missing cast for jsonb functions +const result = await db + .select() + .from(productTable) + .where(sql`${productTable.metadata}->>'category' = 'electronics'`); + +// GOOD - Explicit cast for jsonb operations +const result = await db + .select() + .from(productTable) + .where(sql`${productTable.metadata}::jsonb->>'category' = 'electronics'`); + +// JSON array contains check +const withTag = await db + .select() + .from(productTable) + .where(sql`${productTable.tags}::jsonb ? 'featured'`); + +// JSON array length +const withMultipleTags = await db + .select() + .from(productTable) + .where(sql`jsonb_array_length(${productTable.tags}::jsonb) > 3`); +``` + +### Raw SQL Column Names (camelCase) + +When using raw SQL with Drizzle, column names must use double quotes for camelCase names. + +```typescript +// BAD - PostgreSQL will lowercase unquoted identifiers +await db.execute(sql` + UPDATE order + SET lastUpdatedAt = NOW() + WHERE userId = ${userId} +`); + +// GOOD - Double quotes preserve camelCase +await db.execute(sql` + UPDATE "order" + SET "lastUpdatedAt" = NOW() + WHERE "userId" = ${userId} +`); + +// Complex raw SQL example +await db.execute(sql` + UPDATE "order" AS o + SET + "totalAmount" = sub."calculatedTotal", + "updatedAt" = NOW() + FROM ( + SELECT + "orderId", + SUM("price" * "quantity") AS "calculatedTotal" + FROM "orderItem" + WHERE "orderId" = ANY(${sql.raw(arrayLiteral)}) + GROUP BY "orderId" + ) AS sub + WHERE o.id = sub."orderId" +`); +``` + +### Enum Comparison + +When comparing enum columns in raw SQL, cast the column to text. + +```typescript +// BAD - Direct enum comparison may fail +await db.execute(sql` + SELECT * FROM "order" + WHERE status != 'DRAFT' +`); + +// GOOD - Cast enum column to text +await db.execute(sql` + SELECT * FROM "order" + WHERE status::text != 'DRAFT' +`); + +// In Drizzle query builder (works correctly) +const orders = await db + .select() + .from(orderTable) + .where(ne(orderTable.status, "DRAFT")); +``` + +### Aggregation with Filtering + +Use FILTER clause for conditional aggregation. + +```typescript +await db.execute(sql` + UPDATE "category" AS c + SET + "productCount" = sub."count", + "activeProductCount" = sub."activeCount", + "updatedAt" = NOW() + FROM ( + SELECT + "categoryId", + COUNT(*)::int AS "count", + COUNT(*) FILTER (WHERE "status" = 'ACTIVE')::int AS "activeCount" + FROM "product" + WHERE "categoryId" = ANY(${sql.raw(categoryIds)}) + GROUP BY "categoryId" + ) AS sub + WHERE c.id = sub."categoryId" +`); +``` + +## Transaction Patterns + +### Basic Transaction + +```typescript +import { db } from "@your-app/database"; + +const result = await db.transaction(async (tx) => { + // All operations use tx instead of db + const [order] = await tx + .insert(orderTable) + .values({ userId, total: 0 }) + .returning(); + + await tx.insert(orderItemTable).values( + items.map(item => ({ + orderId: order.id, + ...item, + })) + ); + + // Update order total + const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0); + await tx + .update(orderTable) + .set({ total }) + .where(eq(orderTable.id, order.id)); + + return order; +}); +``` + +### Transaction with Rollback + +```typescript +try { + await db.transaction(async (tx) => { + await tx.insert(orderTable).values(orderData); + + // This will cause rollback if payment fails + const paymentResult = await processPayment(orderData.total); + if (!paymentResult.success) { + throw new Error("Payment failed"); + } + + await tx.update(orderTable) + .set({ paymentId: paymentResult.id }) + .where(eq(orderTable.id, orderData.id)); + }); +} catch (error) { + // Transaction automatically rolled back + logger.error("Order creation failed", { error }); +} +``` + +## Query Performance Tips + +### Use Indexes + +Ensure your queries use appropriate indexes: + +```typescript +// Good for index on (userId, createdAt DESC) +const recentOrders = await db + .select() + .from(orderTable) + .where(eq(orderTable.userId, userId)) + .orderBy(desc(orderTable.createdAt)) + .limit(10); +``` + +### Select Only Needed Columns + +```typescript +// BAD - Selects all columns including large text fields +const orders = await db.select().from(orderTable); + +// GOOD - Select only needed columns +const orders = await db + .select({ + id: orderTable.id, + status: orderTable.status, + total: orderTable.total, + }) + .from(orderTable); +``` + +### Use Relations for Complex Queries + +```typescript +// Using Drizzle relations for nested data +const ordersWithDetails = await db.query.order.findMany({ + where: eq(orderTable.userId, userId), + with: { + items: { + with: { + product: true, + }, + }, + customer: { + columns: { + id: true, + name: true, + email: true, + }, + }, + }, + orderBy: (orders, { desc }) => desc(orders.createdAt), + limit: 20, +}); +``` diff --git a/.trellis/spec/backend/directory-structure.md b/.trellis/spec/backend/directory-structure.md new file mode 100644 index 0000000..043bec0 --- /dev/null +++ b/.trellis/spec/backend/directory-structure.md @@ -0,0 +1,252 @@ +# Directory Structure + +This document describes the module organization pattern for backend API development. + +## Module Structure + +Each API module follows a consistent directory structure: + +``` +packages/api/modules/[module]/ +├── types.ts # Zod schemas and TypeScript types +├── router.ts # Hono router with route definitions +├── lib/ # Core business logic (shared across procedures) +│ ├── client.ts # External service clients +│ └── helpers.ts # Helper functions +├── procedures/ # HTTP endpoint handlers +│ ├── create.ts +│ ├── update.ts +│ ├── delete.ts +│ └── list.ts +└── api/ # API documentation (optional) + ├── create.md + └── list.md +``` + +## File Responsibilities + +### `types.ts` - Schemas and Types + +Define all Zod schemas and TypeScript types for the module. + +```typescript +// types.ts +import { z } from "zod"; + +// Input Schemas +export const createOrderInputSchema = z.object({ + customerId: z.string(), + items: z.array(z.object({ + productId: z.string(), + quantity: z.number().min(1), + })).min(1), +}); + +// Output Schemas +export const orderResponseSchema = z.object({ + success: z.boolean(), + reason: z.string(), + order: z.object({ + id: z.string(), + status: z.string(), + total: z.number(), + }).optional(), +}); + +// Type exports +export type CreateOrderInput = z.infer<typeof createOrderInputSchema>; +export type OrderResponse = z.infer<typeof orderResponseSchema>; +``` + +### `router.ts` - Route Definitions + +The router aggregates all procedures and defines the API routes. + +```typescript +// router.ts +import { Hono } from "hono"; +import { createOrder } from "./procedures/create"; +import { listOrders } from "./procedures/list"; +import { updateOrderStatus } from "./procedures/update"; + +export const ordersRouter = new Hono() + .basePath("/orders") + .post("/", createOrder) + .get("/", listOrders) + .patch("/:id/status", updateOrderStatus); +``` + +### `lib/` - Business Logic + +Contains reusable business logic shared across procedures. + +``` +lib/ +├── client.ts # External API clients (payment gateway, etc.) +├── helpers.ts # Pure helper functions +├── validators.ts # Business rule validators +└── transformers.ts # Data transformation utilities +``` + +**Example: `lib/helpers.ts`** + +```typescript +// lib/helpers.ts +import type { Order } from "../types"; + +/** + * Calculate order total with tax + */ +export function calculateOrderTotal( + items: Array<{ price: number; quantity: number }>, + taxRate: number = 0.1, +): number { + const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0); + return subtotal * (1 + taxRate); +} + +/** + * Generate order reference number + */ +export function generateOrderReference(timestamp: Date): string { + const year = timestamp.getFullYear(); + const month = String(timestamp.getMonth() + 1).padStart(2, "0"); + const random = Math.random().toString(36).substring(2, 8).toUpperCase(); + return `ORD-${year}${month}-${random}`; +} +``` + +### `procedures/` - Endpoint Handlers + +Each procedure handles a single API endpoint with clear responsibilities. + +```typescript +// procedures/create.ts +import { db } from "@your-app/database"; +import { order as orderTable } from "@your-app/database/drizzle/schema/postgres"; +import { logger } from "@your-app/logs"; +import { protectedProcedure } from "../../../orpc/procedures"; +import { calculateOrderTotal, generateOrderReference } from "../lib/helpers"; +import { createOrderInputSchema, orderResponseSchema } from "../types"; + +export const createOrder = protectedProcedure + .route({ + method: "POST", + path: "/orders", + tags: ["Orders"], + summary: "Create a new order", + }) + .input(createOrderInputSchema) + .output(orderResponseSchema) + .handler(async ({ input, context: { user } }) => { + const { customerId, items } = input; + + // Business logic + const total = calculateOrderTotal(items); + const reference = generateOrderReference(new Date()); + + // Database operation + const [newOrder] = await db + .insert(orderTable) + .values({ + userId: user.id, + customerId, + reference, + total, + status: "PENDING", + }) + .returning(); + + logger.info("Order created", { + orderId: newOrder.id, + userId: user.id, + total, + }); + + return { + success: true, + reason: "Order created successfully", + order: { + id: newOrder.id, + status: newOrder.status, + total: newOrder.total, + }, + }; + }); +``` + +### `api/` - Documentation (Optional) + +Markdown documentation for each endpoint, useful for complex APIs. + +```markdown +<!-- api/create.md --> +# Create Order + +## Schema + +### Input +- `customerId`: string - Customer identifier +- `items`: array - Order items + - `productId`: string - Product identifier + - `quantity`: number - Quantity (min: 1) + +### Output +- `success`: boolean +- `reason`: string +- `order`: object (optional) + +## Logic + +1. Validate user has permission to create orders for the customer +2. Verify all products exist and are in stock +3. Calculate total with applicable discounts +4. Create order record +5. Reserve inventory +6. Return order details + +## Usage Example + +```typescript +const result = await api.orders.create({ + customerId: "cust_123", + items: [ + { productId: "prod_456", quantity: 2 }, + ], +}); +``` +``` + +## Naming Conventions + +### Files + +| Type | Convention | Example | +|------|------------|---------| +| Procedures | Verb-based | `create.ts`, `list.ts`, `update-status.ts` | +| Lib files | Noun-based | `helpers.ts`, `validators.ts`, `client.ts` | +| Types | Always `types.ts` | `types.ts` | +| Router | Always `router.ts` | `router.ts` | + +### Exports + +| Type | Convention | Example | +|------|------------|---------| +| Schemas | `{name}Schema` suffix | `createOrderInputSchema` | +| Types | PascalCase | `CreateOrderInput` | +| Procedures | camelCase verb | `createOrder`, `listOrders` | +| Helpers | camelCase verb | `calculateTotal`, `generateReference` | + +## When to Create New Modules + +Create a new module when: + +1. The feature represents a distinct domain entity (users, orders, products) +2. The feature has multiple related operations (CRUD + custom actions) +3. The feature will be reused across multiple routes + +Avoid creating modules for: + +1. Single-use utility functions (place in existing `lib/`) +2. Simple helpers (place in `@your-app/utils`) +3. Database queries only (place in `packages/database/drizzle/queries/`) diff --git a/.trellis/spec/backend/index.md b/.trellis/spec/backend/index.md new file mode 100644 index 0000000..0bc3d2d --- /dev/null +++ b/.trellis/spec/backend/index.md @@ -0,0 +1,154 @@ +# Backend Development Guidelines Index + +> **Tech Stack**: Next.js 15 API Routes + oRPC + Drizzle ORM + PostgreSQL + +## Related Guidelines + +| Guideline | Location | When to Read | +| ------------------------- | ------------ | ---------------------------- | +| **Shared Code Standards** | `../shared/` | Always - applies to all code | + +--- + +## Documentation Files + +| File | Description | When to Read | +| ---------------------------------------------------- | -------------------------------------------------- | ---------------------------------- | +| [directory-structure.md](./directory-structure.md) | Module organization and directory layout | Starting a new feature | +| [orpc-usage.md](./orpc-usage.md) | oRPC router, procedures, middleware patterns | Creating/modifying API endpoints | +| [type-safety.md](./type-safety.md) | Zod schemas, type narrowing, response patterns | Type-related decisions | +| [database.md](./database.md) | Drizzle ORM, queries, transactions, SQL patterns | Database operations | +| [authentication.md](./authentication.md) | better-auth, sessions, OAuth, protected procedures | Auth-related features | +| [logging.md](./logging.md) | Structured logging, Sentry tracing, telemetry | Debugging, observability | +| [local-json-mvp.md](./local-json-mvp.md) | Local JSON persistence/session/API contracts before database adoption | Single-repo MVP features without DB/oRPC deps | +| [performance.md](./performance.md) | Concurrency, caching, batch processing, streaming | Performance optimization | +| [ai-sdk-integration.md](./ai-sdk-integration.md) | Vercel AI SDK, tool calling, prompt patterns | AI-powered features | +| [quality.md](./quality.md) | Pre-commit checklist for backend code | Before committing | + +--- + +## Quick Navigation + +### Service Module Structure + +| Task | File | +| ----------------------------- | -------------------------------------------------- | +| Project structure | [directory-structure.md](./directory-structure.md) | +| Domain module pattern | [directory-structure.md](./directory-structure.md) | +| Write types.ts | [directory-structure.md](./directory-structure.md) | +| Write procedure | [directory-structure.md](./directory-structure.md) | +| Write lib/ helpers | [directory-structure.md](./directory-structure.md) | +| Router setup | [orpc-usage.md](./orpc-usage.md) | +| Middleware composition | [orpc-usage.md](./orpc-usage.md) | +| Naming conventions | [directory-structure.md](./directory-structure.md) | + +### Type Safety + +| Task | File | +| -------------------- | ---------------------------------- | +| Type safety patterns | [type-safety.md](./type-safety.md) | +| Discriminated unions | [type-safety.md](./type-safety.md) | +| Zod-first types | [type-safety.md](./type-safety.md) | +| Zod error handling | [type-safety.md](./type-safety.md) | +| Standard response | [type-safety.md](./type-safety.md) | + +### Database (Drizzle + PostgreSQL) + +| Task | File | +| ----------------------- | ---------------------------- | +| Query organization | [database.md](./database.md) | +| Batch queries (inArray) | [database.md](./database.md) | +| Conflict handling | [database.md](./database.md) | +| Transactions | [database.md](./database.md) | +| JSON column operations | [database.md](./database.md) | +| Raw SQL camelCase | [database.md](./database.md) | +| Enum comparison | [database.md](./database.md) | + +### Error Handling / Logging + +| Task | File | +| --------------------------- | ------------------------------ | +| Structured logging | [logging.md](./logging.md) | +| Sentry span tracing | [logging.md](./logging.md) | +| Error capture | [logging.md](./logging.md) | +| oRPC error codes | [orpc-usage.md](./orpc-usage.md) | +| Batch operation logging | [logging.md](./logging.md) | + +### Performance + +| Task | File | +| ----------------------------- | ---------------------------------- | +| Parallel execution | [performance.md](./performance.md) | +| Concurrency control (p-limit) | [performance.md](./performance.md) | +| Exponential backoff retry | [performance.md](./performance.md) | +| Redis caching | [performance.md](./performance.md) | +| Distributed locks | [performance.md](./performance.md) | +| Chunked batch processing | [performance.md](./performance.md) | +| Streaming large datasets | [performance.md](./performance.md) | + +### Authentication + +| Task | File | +| --------------------------- | -------------------------------------- | +| Protected procedures | [authentication.md](./authentication.md) | +| Admin procedures | [authentication.md](./authentication.md) | +| Session caching (Redis) | [authentication.md](./authentication.md) | +| OAuth integration | [authentication.md](./authentication.md) | +| Role-based access control | [authentication.md](./authentication.md) | +| Client-side auth | [authentication.md](./authentication.md) | + +### AI Integration + +| Task | File | +| --------------------------- | ---------------------------------------------- | +| generateText / generateObject | [ai-sdk-integration.md](./ai-sdk-integration.md) | +| Streaming (streamText) | [ai-sdk-integration.md](./ai-sdk-integration.md) | +| Tool calling | [ai-sdk-integration.md](./ai-sdk-integration.md) | +| Telemetry configuration | [ai-sdk-integration.md](./ai-sdk-integration.md) | +| Prompt engineering | [ai-sdk-integration.md](./ai-sdk-integration.md) | +| AI error handling | [ai-sdk-integration.md](./ai-sdk-integration.md) | + +--- + +## Core Rules Summary + +| Rule | Reference | +| ---------------------------------------------------------------- | -------------------------------------------------- | +| **No `await` in loops** - use `inArray` for batch queries | [database.md](./database.md) | +| **No `console.log`** - use structured logger | [logging.md](./logging.md) | +| **No non-null assertions `!`** - use type narrowing | [type-safety.md](./type-safety.md) | +| **All API inputs/outputs have Zod schemas** | [type-safety.md](./type-safety.md) | +| **Import enums from utils** - not from database package | [type-safety.md](./type-safety.md) | +| **Standard response format** - always include `success`/`reason` | [type-safety.md](./type-safety.md) | +| **Use `protectedProcedure`** for authenticated endpoints | [authentication.md](./authentication.md) | +| **One procedure per file** - keep procedures focused | [orpc-usage.md](./orpc-usage.md) | +| **Service modules follow domain layout** | [directory-structure.md](./directory-structure.md) | +| **Use `Promise.all`** for independent parallel operations | [performance.md](./performance.md) | +| **Use `p-limit`** for external API concurrency control | [performance.md](./performance.md) | +| **Always enable AI telemetry** for token tracking | [ai-sdk-integration.md](./ai-sdk-integration.md) | +| **Cast `::jsonb`** for PostgreSQL JSON operations | [database.md](./database.md) | +| **Double-quote camelCase** column names in raw SQL | [database.md](./database.md) | +| **Use structured context** in logs - no string interpolation | [logging.md](./logging.md) | +| **Run pre-commit checklist** before committing | [quality.md](./quality.md) | + +--- + +## Reference Files + +| Feature | Typical Location | +| -------------------- | --------------------------------------- | +| Drizzle Client | `packages/database/drizzle/client.ts` | +| Schema Definition | `packages/database/drizzle/schema/` | +| Database Queries | `packages/database/drizzle/queries/` | +| oRPC Router | `packages/api/orpc/router.ts` | +| Base Procedures | `packages/api/orpc/procedures.ts` | +| Middleware | `packages/api/orpc/middleware/` | +| Service Module | `packages/api/modules/{domain}/` | +| Module Types (Zod) | `packages/api/modules/{domain}/types.ts`| +| Auth Configuration | `packages/auth/auth.ts` | +| Auth Client | `packages/auth/client.ts` | +| Shared Utils/Enums | `packages/utils/` | + +--- + +**Language**: All documentation must be written in **English**. diff --git a/.trellis/spec/backend/local-json-mvp.md b/.trellis/spec/backend/local-json-mvp.md new file mode 100644 index 0000000..849f565 --- /dev/null +++ b/.trellis/spec/backend/local-json-mvp.md @@ -0,0 +1,86 @@ +# Local JSON MVP Persistence and API Contracts + +## Scenario: Single-Repo MVP Before Database Adoption + +### 1. Scope / Trigger + +- Trigger: implementing full-stack CRUD, auth, RBAC, or audit logging before Drizzle/Postgres/oRPC/better-auth are installed. +- Applies to this single-package Next.js app when the feature must be real and durable in local/runtime execution, but production database infrastructure is not yet available. +- Store boundary: `modules/core/server/store.ts`. + +### 2. Signatures + +- `readData(): Promise<AppData>` +- `writeData(data: AppData): Promise<void>` +- `updateData<T>(mutator: (data: AppData) => T): Promise<T>` +- `getCurrentAuthContext(): Promise<AuthContext | null>` +- `requirePermission(permission: Permission, auditContext: DeniedAuditContext): Promise<PermissionCheckSuccess | PermissionCheckFailure>` +- Route Handlers use standard `GET`, `POST`, `PATCH`, and `DELETE` exports and return `Response`. + +### 3. Contracts + +- Default data file: `.data/teatea.json`. +- Override key: `TEATEA_DATA_DIR` points to the directory containing `teatea.json`. +- `.data/` must stay ignored; it may contain account hashes and session IDs. +- API response shape: + +```ts +type ApiResult<T extends Record<string, unknown>> = + | ({ success: true; reason: string } & T) + | { success: false; reason: string }; +``` + +- Session cookie: + - Name: `teatea_session` + - Flags: `httpOnly`, `sameSite: "lax"`, `path: "/"` + - Lifetime: 7 days +- Passwords: + - Never persist plaintext. + - Use Node crypto salt + scrypt hash until a dedicated auth library is introduced. + +### 4. Validation & Error Matrix + +- Missing/expired session -> `401` with `{ success: false, reason: "未登录或会话已过期" }`. +- Missing permission -> `403` with `{ success: false, reason: "权限不足" }` and a denied audit log. +- Invalid JSON body -> `400` with a Chinese user-facing `reason`. +- Missing record by ID -> `404` with `{ success: false, reason: "<entity>不存在" }`. +- Duplicate account email -> `409` with `{ success: false, reason: "账号已存在" }`. + +### 5. Good/Base/Bad Cases + +- Good: UI submits to Route Handler, Route Handler validates input, calls `requirePermission`, mutates data through `updateData`, writes audit log, returns `ApiResult`. +- Base: Server Component reads data directly via `readData` after session/permission checks. +- Bad: UI or route handler reads `.data/teatea.json` directly, bypasses `requirePermission`, or stores auth state in localStorage. + +### 6. Tests Required + +- `pnpm lint` +- `pnpm type-check` +- `pnpm build` +- Manual or automated integration assertions: + - first setup creates admin and cookie session + - protected app route redirects without cookie + - CRUD mutation persists across reload/API list + - denied permission returns 403 and writes an audit log + +### 7. Wrong vs Correct + +#### Wrong + +```ts +window.localStorage.setItem("teatea.session", JSON.stringify(session)); +``` + +#### Correct + +```ts +const cookieStore = await cookies(); +cookieStore.set({ + name: "teatea_session", + value: sessionId, + httpOnly: true, + sameSite: "lax", + path: "/", +}); +``` + diff --git a/.trellis/spec/backend/logging.md b/.trellis/spec/backend/logging.md new file mode 100644 index 0000000..7582604 --- /dev/null +++ b/.trellis/spec/backend/logging.md @@ -0,0 +1,340 @@ +# Logging and Monitoring + +This document covers structured logging, error tracking with Sentry, and observability patterns. + +## Critical Rules + +### NO `console.log` - Use Structured Logger + +Never use `console.log` in production code. Always use the structured logger from `@your-app/logs`. + +```typescript +// BAD - Unstructured console logging +console.log("Order created:", orderId); +console.error("Failed to process:", error); + +// GOOD - Structured logging +import { logger } from "@your-app/logs"; + +logger.info("Order created", { + orderId, + userId, + total: order.total, +}); + +logger.error("Failed to process order", { + orderId, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, +}); +``` + +## Logger API + +```typescript +import { logger } from "@your-app/logs"; + +// Log levels +logger.debug("Debug message", { context: "value" }); +logger.info("Info message", { orderId, status }); +logger.warn("Warning message", { userId, reason: "quota exceeded" }); +logger.error("Error message", { error: err.message, stack: err.stack }); +``` + +## Sentry Integration + +### Span Tracing + +Use the tracing system to monitor performance and track operations. + +```typescript +import { SpanPrefix, span } from "../../../lib/tracer"; + +// Database operations +const orders = await span( + `${SpanPrefix.DB}GetUserOrders`, + () => db.select().from(orderTable).where(eq(orderTable.userId, userId)), + { userId, limit: 20 } +); + +// External API calls +const response = await span( + `${SpanPrefix.Http}FetchInventory`, + () => inventoryClient.getStock(productIds), + { productCount: productIds.length } +); + +// Redis cache operations +const cached = await span( + `${SpanPrefix.Redis}GetSession`, + () => redis.get(sessionKey), + { sessionKey } +); +``` + +### SpanPrefix Constants + +Use standardized prefixes for consistent Sentry categorization: + +```typescript +import { SpanPrefix } from "../../../lib/tracer"; + +const SpanPrefix = { + /** Database operations - maps to Sentry op: db.query */ + DB: "DB.", + + /** External HTTP API calls - maps to Sentry op: http.client */ + Http: "Http.", + + /** Redis cache operations - maps to Sentry op: db.redis */ + Redis: "Redis.", + + /** AI model invocations - maps to Sentry op: ai.run */ + AI: "AI.", + + /** Generic cache operations - maps to Sentry op: cache */ + Cache: "Cache.", + + /** Queue/message operations - maps to Sentry op: queue */ + Queue: "Queue.", +} as const; +``` + +### Naming Convention + +```typescript +// Pattern: ${SpanPrefix.Type}${Action}${Resource} + +// Database +`${SpanPrefix.DB}GetUserOrders` +`${SpanPrefix.DB}BatchUpdateProducts` +`${SpanPrefix.DB}CreateOrder` + +// External APIs +`${SpanPrefix.Http}FetchPaymentStatus` +`${SpanPrefix.Http}SendNotification` + +// Redis +`${SpanPrefix.Redis}GetSession` +`${SpanPrefix.Redis}SetCache` + +// AI +`${SpanPrefix.AI}ClassifyContent` +`${SpanPrefix.AI}GenerateResponse` +``` + +### Error Capture + +```typescript +import { captureError } from "../../../lib/tracer"; + +try { + await processOrder(orderId); +} catch (error) { + captureError(error, { + tags: { + operation: "processOrder", + orderId, + }, + extra: { + userId: context.user.id, + orderStatus: order.status, + }, + }); + + throw error; // Re-throw if needed +} +``` + +### Trace Context + +For complex operations, use trace context to correlate logs: + +```typescript +import { runWithTrace, getLogId } from "../../../lib/tracer"; + +export async function processOrderBatch(orderIds: string[]) { + return runWithTrace(`batch-${Date.now()}`, async () => { + const logId = getLogId(); + + logger.info("Starting batch processing", { + logId, + orderCount: orderIds.length + }); + + for (const orderId of orderIds) { + await span( + `${SpanPrefix.DB}ProcessOrder`, + () => processSingleOrder(orderId), + { orderId } + ); + } + + logger.info("Batch processing complete", { logId }); + }); +} +``` + +## AI SDK Telemetry + +When using the Vercel AI SDK, enable telemetry for token tracking: + +```typescript +import { generateText } from "ai"; +import { openai } from "@ai-sdk/openai"; + +const result = await generateText({ + model: openai("gpt-4o"), + prompt: userPrompt, + experimental_telemetry: { + isEnabled: true, + functionId: "classify-content", + metadata: { + userId, + contentLength: content.length, + }, + }, +}); +``` + +### Telemetry Metadata + +Include relevant context in telemetry: + +```typescript +experimental_telemetry: { + isEnabled: true, + functionId: "generate-response", // Unique identifier for this AI function + metadata: { + // User context + userId: context.user.id, + + // Input metrics + promptTokens: estimatedTokens, + + // Business context + feature: "auto-reply", + priority: "high", + }, +} +``` + +## Error Handling Patterns + +### Structured Error Logging + +```typescript +async function processPayment(orderId: string) { + try { + const result = await paymentGateway.charge(orderId); + + logger.info("Payment processed", { + orderId, + transactionId: result.transactionId, + amount: result.amount, + }); + + return result; + } catch (error) { + logger.error("Payment processing failed", { + orderId, + error: error instanceof Error ? error.message : String(error), + errorCode: (error as any).code, + }); + + // Capture to Sentry with context + captureError(error, { + tags: { service: "payment", operation: "charge" }, + extra: { orderId }, + }); + + throw new ORPCError("INTERNAL_SERVER_ERROR", { + message: "Payment processing failed", + }); + } +} +``` + +### Batch Operation Logging + +```typescript +async function batchUpdateInventory(updates: InventoryUpdate[]) { + const results: ProcessResult[] = []; + + logger.info("Starting batch inventory update", { + updateCount: updates.length, + }); + + const processed = await Promise.allSettled( + updates.map(update => processUpdate(update)) + ); + + const successful = processed.filter(r => r.status === "fulfilled").length; + const failed = processed.filter(r => r.status === "rejected").length; + + logger.info("Batch inventory update complete", { + total: updates.length, + successful, + failed, + }); + + if (failed > 0) { + logger.warn("Some inventory updates failed", { + failedCount: failed, + errors: processed + .filter((r): r is PromiseRejectedResult => r.status === "rejected") + .map(r => r.reason?.message || "Unknown error"), + }); + } + + return { successful, failed }; +} +``` + +## Logging Best Practices + +### What to Log + +**Always log:** +- Request/response for external API calls +- Database write operations (create, update, delete) +- Authentication events +- Business-critical operations +- Errors and exceptions + +**Log with care (avoid sensitive data):** +- User inputs (sanitize PII) +- Request payloads (redact secrets) + +**Never log:** +- Passwords or tokens +- Credit card numbers +- Personal identification numbers +- API keys or secrets + +### Log Levels Guide + +| Level | Use Case | Example | +|-------|----------|---------| +| `debug` | Development diagnostics | Variable values, flow tracing | +| `info` | Normal operations | Order created, user logged in | +| `warn` | Recoverable issues | Rate limit approaching, retry attempted | +| `error` | Failures requiring attention | API call failed, database error | + +### Structured Context + +Always include relevant context as structured data: + +```typescript +// BAD - String interpolation +logger.info(`User ${userId} created order ${orderId} for $${total}`); + +// GOOD - Structured context +logger.info("Order created", { + userId, + orderId, + total, + currency: "USD", + itemCount: items.length, +}); +``` diff --git a/.trellis/spec/backend/orpc-usage.md b/.trellis/spec/backend/orpc-usage.md new file mode 100644 index 0000000..53ec3d9 --- /dev/null +++ b/.trellis/spec/backend/orpc-usage.md @@ -0,0 +1,805 @@ +# oRPC Backend Usage Guidelines + +## 1. Overview + +### What is oRPC + +oRPC (OpenAPI RPC) is a type-safe RPC framework for TypeScript that provides end-to-end type safety from your backend to frontend. It combines the best aspects of REST APIs and RPC frameworks while generating OpenAPI specifications automatically. + +### Why oRPC over tRPC or plain REST + +| Feature | oRPC | tRPC | REST | +|---------|------|------|------| +| Type Safety | End-to-end | End-to-end | Manual | +| OpenAPI Generation | Built-in | Plugin required | Manual | +| HTTP Method Control | Full control | Limited | Full control | +| Learning Curve | Low | Low | Medium | +| Middleware Support | Native | Native | Framework-dependent | +| Schema Validation | Zod native | Zod native | Manual | + +Key advantages of oRPC: +- **OpenAPI-first**: Automatic OpenAPI spec generation for documentation and client generation +- **HTTP semantics**: Full control over HTTP methods, paths, and tags +- **Type inference**: Automatic TypeScript types from Zod schemas +- **Middleware composition**: Chainable middleware for auth, logging, etc. + +### Project Structure with oRPC + +``` +packages/api/ +├── orpc/ +│ ├── router.ts # Main router composition +│ ├── procedures.ts # Base procedure definitions +│ └── middleware/ # Reusable middleware +│ ├── log-id-middleware.ts +│ └── locale-middleware.ts +├── modules/ +│ └── [module]/ +│ ├── router.ts # Module router exports +│ ├── types.ts # Zod schemas and TypeScript types +│ └── procedures/ # Individual procedure implementations +│ ├── create-item.ts +│ ├── list-items.ts +│ └── update-item.ts +└── lib/ # Shared utilities +``` + +## 2. Router Setup + +### Main Router Structure + +The main router composes all module routers under a common prefix: + +```typescript +// orpc/router.ts +import type { RouterClient } from "@orpc/server"; +import { usersRouter } from "../modules/users/router"; +import { itemsRouter } from "../modules/items/router"; +import { publicProcedure } from "./procedures"; + +export const router = publicProcedure + // Prefix for OpenAPI paths + .prefix("/api") + .router({ + users: usersRouter, + items: itemsRouter, + // Add more module routers here + }); + +// Export type for frontend client +export type ApiRouterClient = RouterClient<typeof router>; +``` + +### Module Router Composition + +Each module exports a router object that groups related procedures: + +```typescript +// modules/items/router.ts +import { createItem } from "./procedures/create-item"; +import { deleteItem } from "./procedures/delete-item"; +import { findItem } from "./procedures/find-item"; +import { listItems } from "./procedures/list-items"; +import { updateItem } from "./procedures/update-item"; + +export const itemsRouter = { + list: listItems, + find: findItem, + create: createItem, + update: updateItem, + delete: deleteItem, + // Nested routes are supported + drafts: { + list: listDrafts, + save: saveDraft, + }, +}; +``` + +### Base Procedures with Middleware + +Define base procedures with common middleware: + +```typescript +// orpc/procedures.ts +import { ORPCError, os } from "@orpc/server"; +import { logIdMiddleware } from "./middleware/log-id-middleware"; + +// Public procedure - no authentication required +export const publicProcedure = os + .$context<{ + headers: Headers; + }>() + .use(logIdMiddleware); + +// Protected procedure - requires authentication +export const protectedProcedure = publicProcedure.use( + async ({ context, next }) => { + const session = await getSession(context.headers); + + if (!session) { + throw new ORPCError("UNAUTHORIZED"); + } + + return await next({ + context: { + session: session.session, + user: session.user, + }, + }); + }, +); + +// Admin procedure - requires admin role +export const adminProcedure = protectedProcedure.use( + async ({ context, next }) => { + if (context.user.role !== "admin") { + throw new ORPCError("FORBIDDEN"); + } + + return await next(); + }, +); +``` + +## 3. Procedure Definition + +### Query Procedures (GET-like) + +Use GET method for read operations that don't modify data: + +```typescript +// modules/items/procedures/list-items.ts +import { z } from "zod"; +import { protectedProcedure } from "../../../orpc/procedures"; + +// Define input schema +const listItemsInputSchema = z.object({ + limit: z.number().min(1).max(100).default(50), + cursor: z.object({ + createdAt: z.string(), + id: z.string(), + }).optional(), + filters: z.object({ + status: z.enum(["active", "archived"]).optional(), + category: z.string().optional(), + }).optional(), +}); + +// Define output schema +const listItemsOutputSchema = z.object({ + items: z.array(z.object({ + id: z.string(), + name: z.string(), + status: z.string(), + createdAt: z.date(), + })), + nextCursor: z.object({ + createdAt: z.string(), + id: z.string(), + }).nullable(), + hasMore: z.boolean(), +}); + +export const listItems = protectedProcedure + .route({ + method: "GET", + path: "/items", + tags: ["Items"], + summary: "List items with cursor pagination", + description: "Retrieve a paginated list of items for the current user", + }) + .input(listItemsInputSchema) + .output(listItemsOutputSchema) + .handler(async ({ input, context }) => { + const { limit, cursor, filters } = input; + const { user } = context; + + // Query implementation + const items = await db.query.items.findMany({ + where: { userId: user.id, ...filters }, + limit: limit + 1, // Fetch one extra to check hasMore + orderBy: [desc(items.createdAt), desc(items.id)], + }); + + const hasMore = items.length > limit; + const resultItems = hasMore ? items.slice(0, limit) : items; + + return { + items: resultItems, + nextCursor: hasMore && resultItems.length > 0 + ? { + createdAt: resultItems[resultItems.length - 1].createdAt.toISOString(), + id: resultItems[resultItems.length - 1].id, + } + : null, + hasMore, + }; + }); +``` + +### Mutation Procedures (POST/PUT/DELETE-like) + +Use POST for create operations, PUT/PATCH for updates, DELETE for removals: + +```typescript +// modules/items/procedures/create-item.ts +import { ORPCError } from "@orpc/client"; +import { z } from "zod"; +import { protectedProcedure } from "../../../orpc/procedures"; + +const createItemInputSchema = z.object({ + name: z.string().min(1).max(255), + description: z.string().optional(), + categoryId: z.string().optional(), +}); + +const createItemOutputSchema = z.object({ + item: z.object({ + id: z.string(), + name: z.string(), + description: z.string().nullable(), + createdAt: z.date(), + }), +}); + +export const createItem = protectedProcedure + .route({ + method: "POST", + path: "/items", + tags: ["Items"], + summary: "Create a new item", + }) + .input(createItemInputSchema) + .output(createItemOutputSchema) + .handler(async ({ input, context }) => { + const { name, description, categoryId } = input; + const { user } = context; + + // Validate category if provided + if (categoryId) { + const category = await db.query.categories.findFirst({ + where: { id: categoryId, userId: user.id }, + }); + if (!category) { + throw new ORPCError("NOT_FOUND", { + message: "Category not found", + }); + } + } + + const item = await db.insert(items).values({ + name, + description, + categoryId, + userId: user.id, + }).returning(); + + return { item: item[0] }; + }); +``` + +### Update Procedure Example + +```typescript +// modules/items/procedures/update-item.ts +import { ORPCError } from "@orpc/client"; +import { z } from "zod"; +import { protectedProcedure } from "../../../orpc/procedures"; + +const updateItemInputSchema = z.object({ + itemId: z.string(), + name: z.string().min(1).max(255).optional(), + description: z.string().optional(), + status: z.enum(["active", "archived"]).optional(), +}); + +export const updateItem = protectedProcedure + .route({ + method: "PUT", + path: "/items/{itemId}", + tags: ["Items"], + summary: "Update an item", + }) + .input(updateItemInputSchema) + .handler(async ({ input, context }) => { + const { itemId, ...updates } = input; + const { user } = context; + + // Verify ownership + const existingItem = await db.query.items.findFirst({ + where: { id: itemId }, + }); + + if (!existingItem) { + throw new ORPCError("NOT_FOUND", { message: "Item not found" }); + } + + if (existingItem.userId !== user.id) { + throw new ORPCError("FORBIDDEN", { + message: "You don't have permission to modify this item", + }); + } + + const updated = await db.update(items) + .set({ ...updates, updatedAt: new Date() }) + .where(eq(items.id, itemId)) + .returning(); + + return { item: updated[0] }; + }); +``` + +### Input Validation with Zod + +oRPC uses Zod for input validation. Define schemas in a separate `types.ts` file for reusability: + +```typescript +// modules/items/types.ts +import { z } from "zod"; + +// Input Schemas +export const createItemInputSchema = z.object({ + name: z.string().min(1).max(255), + description: z.string().max(1000).optional(), + tags: z.array(z.string()).max(10).optional(), +}); + +export const updateItemInputSchema = z.object({ + itemId: z.string(), + name: z.string().min(1).max(255).optional(), + description: z.string().max(1000).optional(), +}); + +export const listItemsInputSchema = z.object({ + limit: z.number().int().min(1).max(100).default(50), + cursor: z.object({ + createdAt: z.string(), + id: z.string(), + }).optional(), +}); + +// Output Schemas +export const itemSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string().nullable(), + status: z.enum(["active", "archived"]), + createdAt: z.date(), + updatedAt: z.date(), +}); + +export const operationResultSchema = z.object({ + success: z.boolean(), +}); + +export const batchOperationResultSchema = z.object({ + success: z.boolean(), + successCount: z.number(), + failedCount: z.number(), + failedIds: z.array(z.string()).optional(), +}); + +// Type exports (inferred from schemas) +export type CreateItemInput = z.infer<typeof createItemInputSchema>; +export type UpdateItemInput = z.infer<typeof updateItemInputSchema>; +export type ListItemsInput = z.infer<typeof listItemsInputSchema>; +export type Item = z.infer<typeof itemSchema>; +export type OperationResult = z.infer<typeof operationResultSchema>; +``` + +## 4. Middleware + +### Authentication Middleware + +Built into `protectedProcedure`: + +```typescript +// orpc/procedures.ts +export const protectedProcedure = publicProcedure.use( + async ({ context, next }) => { + const session = await getSession(context.headers); + + if (!session) { + throw new ORPCError("UNAUTHORIZED"); + } + + // Add user info to context for downstream handlers + return await next({ + context: { + session: session.session, + user: session.user, + }, + }); + }, +); +``` + +### Logging Middleware + +Generate and propagate request IDs for tracing: + +```typescript +// orpc/middleware/log-id-middleware.ts +import { os } from "@orpc/server"; + +function generateLogId(): string { + return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`; +} + +function getOrGenerateLogId(headers: Headers): string { + // Prefer client-provided x-log-id for distributed tracing + const existingLogId = headers.get("x-log-id"); + if (existingLogId) { + return existingLogId; + } + return generateLogId(); +} + +export const logIdMiddleware = os + .$context<{ + headers: Headers; + }>() + .middleware(async ({ context, next }) => { + const logId = getOrGenerateLogId(context.headers); + + // Run with tracing context + return await runWithTrace(logId, async () => { + return await next({ + context: { + logId, + }, + }); + }); + }); +``` + +### Locale Middleware + +Extract locale from cookies for i18n: + +```typescript +// orpc/middleware/locale-middleware.ts +import { os } from "@orpc/server"; +import { getCookie } from "@orpc/server/helpers"; +import { config } from "@your-app/config"; +import type { Locale } from "@your-app/i18n"; + +export const localeMiddleware = os + .$context<{ + headers: Headers; + }>() + .middleware(async ({ context, next }) => { + const locale = (getCookie( + context.headers, + config.i18n.localeCookieName, + ) as Locale) ?? config.i18n.defaultLocale; + + return await next({ + context: { + locale, + }, + }); + }); +``` + +### Using Middleware in Procedures + +Apply middleware to specific procedures: + +```typescript +// modules/contact/procedures/submit-contact-form.ts +import { localeMiddleware } from "../../../orpc/middleware/locale-middleware"; +import { publicProcedure } from "../../../orpc/procedures"; + +export const submitContactForm = publicProcedure + .route({ + method: "POST", + path: "/contact", + tags: ["Contact"], + summary: "Submit contact form", + }) + .input(contactFormSchema) + .use(localeMiddleware) // Apply locale middleware + .handler(async ({ input, context: { locale } }) => { + // locale is now available in context + await sendEmail({ + to: config.contactForm.to, + locale, + subject: config.contactForm.subject, + text: `Name: ${input.name}\n\nEmail: ${input.email}\n\nMessage: ${input.message}`, + }); + }); +``` + +### Error Handling Middleware + +Create custom error handling: + +```typescript +// orpc/middleware/error-middleware.ts +import { ORPCError, os } from "@orpc/server"; +import { logger } from "@your-app/logs"; + +export const errorMiddleware = os.middleware(async ({ next, path }) => { + try { + return await next(); + } catch (error) { + // Log error with context + logger.error("Procedure error", { + path, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + + // Re-throw oRPC errors as-is + if (error instanceof ORPCError) { + throw error; + } + + // Wrap unknown errors + throw new ORPCError("INTERNAL_SERVER_ERROR", { + message: "An unexpected error occurred", + }); + } +}); +``` + +## 5. Context + +### How to Access User Session + +The session is available in context after `protectedProcedure`: + +```typescript +export const getProfile = protectedProcedure + .route({ method: "GET", path: "/users/profile", tags: ["Users"] }) + .handler(async ({ context }) => { + // context.user contains the authenticated user + const { user, session } = context; + + return { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + sessionId: session.id, + }; + }); +``` + +### How to Access Logger + +Use the logger from the shared logs package: + +```typescript +import { logger } from "@your-app/logs"; + +export const createItem = protectedProcedure + .route({ method: "POST", path: "/items", tags: ["Items"] }) + .input(createItemInputSchema) + .handler(async ({ input, context }) => { + logger.info("Creating item", { + userId: context.user.id, + itemName: input.name, + }); + + try { + const item = await db.insert(items).values({ + ...input, + userId: context.user.id, + }).returning(); + + logger.info("Item created successfully", { itemId: item[0].id }); + return { item: item[0] }; + } catch (error) { + logger.error("Failed to create item", { + userId: context.user.id, + error: error instanceof Error ? error.message : String(error), + }); + throw new ORPCError("INTERNAL_SERVER_ERROR", { + message: "Failed to create item", + }); + } + }); +``` + +### How to Access Database + +Import the database client and use it directly: + +```typescript +import { db } from "@your-app/database"; +import { items, categories } from "@your-app/database/drizzle/schema"; +import { eq, and, desc } from "drizzle-orm"; + +export const listItems = protectedProcedure + .route({ method: "GET", path: "/items", tags: ["Items"] }) + .handler(async ({ context }) => { + // Using Drizzle query builder + const userItems = await db.query.items.findMany({ + where: eq(items.userId, context.user.id), + orderBy: desc(items.createdAt), + with: { + category: true, // Include relations + }, + }); + + // Or using raw select + const itemsWithCategory = await db + .select({ + id: items.id, + name: items.name, + categoryName: categories.name, + }) + .from(items) + .leftJoin(categories, eq(items.categoryId, categories.id)) + .where(eq(items.userId, context.user.id)); + + return { items: userItems }; + }); +``` + +## 6. Best Practices + +### Input/Output Schema Naming Conventions + +Follow consistent naming patterns: + +```typescript +// Input schemas: [action][Entity]InputSchema +export const createItemInputSchema = z.object({ ... }); +export const updateItemInputSchema = z.object({ ... }); +export const listItemsInputSchema = z.object({ ... }); +export const deleteItemInputSchema = z.object({ ... }); + +// Output schemas: [action][Entity]OutputSchema or [entity]Schema +export const itemSchema = z.object({ ... }); +export const listItemsOutputSchema = z.object({ ... }); +export const operationResultSchema = z.object({ ... }); + +// Shared/reusable schemas: [entity]Schema or [concept]Schema +export const paginationSchema = z.object({ + limit: z.number().int().min(1).max(100).default(50), + cursor: z.object({ + createdAt: z.string(), + id: z.string(), + }).optional(), +}); +``` + +### Error Handling Patterns + +Use appropriate error codes and messages: + +```typescript +import { ORPCError } from "@orpc/client"; + +// Resource not found +throw new ORPCError("NOT_FOUND", { + message: "Item not found", +}); + +// Permission denied +throw new ORPCError("FORBIDDEN", { + message: "You don't have permission to access this resource", +}); + +// Authentication required +throw new ORPCError("UNAUTHORIZED", { + message: "Please sign in to continue", +}); + +// Validation error (usually handled by Zod, but for custom validation) +throw new ORPCError("BAD_REQUEST", { + message: "Invalid email format", +}); + +// Conflict (e.g., duplicate entry) +throw new ORPCError("CONFLICT", { + message: "An item with this name already exists", +}); + +// Server error (wrap internal errors) +try { + await externalService.call(); +} catch (error) { + logger.error("External service failed", { error }); + throw new ORPCError("INTERNAL_SERVER_ERROR", { + message: "Service temporarily unavailable", + }); +} +``` + +### Procedure Organization + +1. **One procedure per file**: Keep procedures focused and testable +2. **Group related procedures**: Use module routers to organize by domain +3. **Reuse schemas**: Define common schemas in `types.ts` +4. **Consistent file naming**: Use kebab-case matching the procedure name + +``` +modules/items/ +├── router.ts # Exports all procedures +├── types.ts # Shared schemas and types +└── procedures/ + ├── create-item.ts # createItem procedure + ├── delete-item.ts # deleteItem procedure + ├── find-item.ts # findItem procedure + ├── list-items.ts # listItems procedure + └── update-item.ts # updateItem procedure +``` + +### Performance Tips + +1. **Use cursor pagination** instead of offset for large datasets +2. **Batch database queries** to avoid N+1 problems +3. **Add appropriate indexes** for filtered/sorted columns +4. **Use select** to fetch only needed columns + +```typescript +// Avoid N+1 queries - fetch related data in batch +const items = await db.query.items.findMany({ + where: eq(items.userId, user.id), + limit, +}); + +// Batch fetch labels for all items +const itemIds = items.map(i => i.id); +const labels = await db.query.itemLabels.findMany({ + where: inArray(itemLabels.itemId, itemIds), +}); + +// Group labels by itemId +const labelsByItemId = new Map(); +for (const label of labels) { + const existing = labelsByItemId.get(label.itemId) || []; + existing.push(label); + labelsByItemId.set(label.itemId, existing); +} + +// Combine results +const itemsWithLabels = items.map(item => ({ + ...item, + labels: labelsByItemId.get(item.id) || [], +})); +``` + +### Testing Procedures + +Structure tests to cover various scenarios: + +```typescript +import { describe, expect, it } from "vitest"; +import { createCaller } from "../test-utils"; + +describe("createItem", () => { + it("creates an item successfully", async () => { + const caller = createCaller({ user: testUser }); + + const result = await caller.items.create({ + name: "Test Item", + description: "A test item", + }); + + expect(result.item.name).toBe("Test Item"); + expect(result.item.id).toBeDefined(); + }); + + it("throws UNAUTHORIZED for unauthenticated users", async () => { + const caller = createCaller({ user: null }); + + await expect( + caller.items.create({ name: "Test" }) + ).rejects.toThrow("UNAUTHORIZED"); + }); + + it("validates input schema", async () => { + const caller = createCaller({ user: testUser }); + + await expect( + caller.items.create({ name: "" }) // Empty name + ).rejects.toThrow(); + }); +}); +``` diff --git a/.trellis/spec/backend/performance.md b/.trellis/spec/backend/performance.md new file mode 100644 index 0000000..0696cc2 --- /dev/null +++ b/.trellis/spec/backend/performance.md @@ -0,0 +1,498 @@ +# Performance Patterns + +This document covers performance optimization patterns for backend development. + +## Parallel Execution with Promise.all + +When operations are independent, execute them in parallel. + +```typescript +// BAD - Sequential execution (slow) +const user = await getUser(userId); +const orders = await getOrders(userId); +const preferences = await getPreferences(userId); + +// GOOD - Parallel execution +const [user, orders, preferences] = await Promise.all([ + getUser(userId), + getOrders(userId), + getPreferences(userId), +]); +``` + +### Promise.allSettled for Partial Failures + +When some operations can fail without blocking others: + +```typescript +const results = await Promise.allSettled([ + processOrderA(), + processOrderB(), + processOrderC(), +]); + +const successful = results + .filter((r): r is PromiseFulfilledResult<Order> => r.status === "fulfilled") + .map(r => r.value); + +const failed = results + .filter((r): r is PromiseRejectedResult => r.status === "rejected") + .map(r => r.reason); + +logger.info("Batch processing complete", { + successful: successful.length, + failed: failed.length, +}); +``` + +## Concurrency Control with p-limit + +When calling external APIs, limit concurrent requests to avoid rate limiting. + +```typescript +import pLimit from "p-limit"; + +// Create limiter with max 20 concurrent requests +const limit = pLimit(20); + +const orderIds = ["order1", "order2", /* ... hundreds more */]; + +// Process all with controlled concurrency +const results = await Promise.all( + orderIds.map(orderId => + limit(() => fetchOrderDetails(orderId)) + ) +); +``` + +### Shared Limiter Pattern + +For module-wide concurrency control: + +```typescript +// lib/api-client.ts +import pLimit from "p-limit"; + +// External API concurrency limit +const API_CONCURRENCY = 20; + +export function createApiLimiter(): ReturnType<typeof pLimit> { + return pLimit(API_CONCURRENCY); +} + +// Usage in procedure +const limiter = createApiLimiter(); + +const results = await Promise.allSettled( + items.map(item => + limiter(async () => { + try { + const result = await externalApi.process(item); + return { itemId: item.id, success: true, result }; + } catch (error) { + return { + itemId: item.id, + success: false, + error: error instanceof Error ? error.message : "Unknown error" + }; + } + }) + ) +); +``` + +## Rate Limit Retry with Exponential Backoff + +Handle rate limits gracefully with automatic retry. + +```typescript +const MAX_RETRIES = 3; + +async function fetchWithRetry<T>( + fn: () => Promise<T>, + context: { operation: string; itemId: string } +): Promise<T> { + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + return await fn(); + } catch (error: any) { + const isRateLimited = error?.code === 429 || error?.status === 429; + + if (isRateLimited && attempt < MAX_RETRIES) { + // Exponential backoff: 2^attempt seconds + random jitter + const delay = 2 ** attempt * 1000 + Math.random() * 1000; + + logger.warn("Rate limited, retrying", { + operation: context.operation, + itemId: context.itemId, + attempt, + delay: Math.round(delay), + }); + + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + + throw error; + } + } + + throw new Error(`Failed after ${MAX_RETRIES} attempts`); +} + +// Usage +const result = await fetchWithRetry( + () => externalApi.getResource(resourceId), + { operation: "getResource", itemId: resourceId } +); +``` + +### Backoff Configuration + +```typescript +interface RetryConfig { + maxRetries: number; + baseDelay: number; // Base delay in ms + maxDelay: number; // Maximum delay cap + jitterFactor: number; // Random jitter (0-1) +} + +const defaultConfig: RetryConfig = { + maxRetries: 3, + baseDelay: 1000, + maxDelay: 30000, + jitterFactor: 0.5, +}; + +function calculateDelay(attempt: number, config: RetryConfig): number { + const exponentialDelay = config.baseDelay * 2 ** (attempt - 1); + const cappedDelay = Math.min(exponentialDelay, config.maxDelay); + const jitter = cappedDelay * config.jitterFactor * Math.random(); + return cappedDelay + jitter; +} +``` + +## Redis Caching (Cache-Aside Pattern) + +Implement caching for expensive operations. + +```typescript +import { redis } from "../../../lib/redis"; +import { SpanPrefix, span } from "../../../lib/tracer"; + +const CACHE_TTL = 3600; // 1 hour in seconds + +interface CachedUserProfile { + id: string; + name: string; + preferences: Record<string, unknown>; +} + +async function getUserProfile(userId: string): Promise<CachedUserProfile> { + const cacheKey = `user:profile:${userId}`; + + // 1. Try cache first + const cached = await span( + `${SpanPrefix.Redis}GetUserProfile`, + async () => { + const data = await redis.get<string>(cacheKey); + return data ? JSON.parse(data) as CachedUserProfile : null; + }, + { userId } + ); + + if (cached) { + return cached; + } + + // 2. Cache miss - fetch from database + const profile = await span( + `${SpanPrefix.DB}FetchUserProfile`, + () => db.query.user.findFirst({ + where: eq(userTable.id, userId), + with: { preferences: true }, + }), + { userId } + ); + + if (!profile) { + throw new ORPCError("NOT_FOUND", { message: "User not found" }); + } + + const cacheValue: CachedUserProfile = { + id: profile.id, + name: profile.name, + preferences: profile.preferences, + }; + + // 3. Store in cache + await span( + `${SpanPrefix.Redis}SetUserProfile`, + () => redis.set(cacheKey, JSON.stringify(cacheValue), { ex: CACHE_TTL }), + { userId } + ); + + return cacheValue; +} +``` + +### Cache Invalidation + +```typescript +async function updateUserProfile( + userId: string, + updates: Partial<UserProfile> +): Promise<void> { + // 1. Update database + await db.update(userTable) + .set(updates) + .where(eq(userTable.id, userId)); + + // 2. Invalidate cache + const cacheKey = `user:profile:${userId}`; + await redis.del(cacheKey); + + logger.info("User profile updated and cache invalidated", { userId }); +} +``` + +### Cache Key Patterns + +```typescript +// User-specific data +`user:profile:${userId}` +`user:settings:${userId}` +`user:orders:${userId}:page:${page}` + +// Resource-specific data +`product:${productId}` +`inventory:${warehouseId}:${productId}` + +// Aggregated data +`stats:daily:${date}` +`leaderboard:${category}` +``` + +## Background Tasks with Distributed Locks + +Prevent duplicate processing in distributed environments. + +```typescript +const LOCK_KEY = "task:process-orders"; +const LOCK_TTL = 300; // 5 minutes + +async function processScheduledOrders(): Promise<void> { + // 1. Try to acquire lock + const lockResult = await redis.set(LOCK_KEY, Date.now(), { + ex: LOCK_TTL, + nx: true, // Only set if not exists + }); + + if (!lockResult) { + logger.info("Another instance is processing orders, skipping"); + return; + } + + try { + // 2. Process with lock held + logger.info("Acquired lock, processing scheduled orders"); + + const pendingOrders = await db + .select() + .from(orderTable) + .where(and( + eq(orderTable.status, "SCHEDULED"), + lte(orderTable.scheduledAt, new Date()) + )) + .limit(100); + + for (const order of pendingOrders) { + await processOrder(order); + } + + logger.info("Scheduled orders processed", { + count: pendingOrders.length + }); + } finally { + // 3. Release lock + await redis.del(LOCK_KEY); + } +} +``` + +### Lock with Heartbeat + +For long-running tasks, extend the lock periodically: + +```typescript +async function processLongRunningTask(): Promise<void> { + const LOCK_KEY = "task:long-running"; + const LOCK_TTL = 30; + const HEARTBEAT_INTERVAL = 10000; // 10 seconds + + const lockResult = await redis.set(LOCK_KEY, Date.now(), { + ex: LOCK_TTL, + nx: true, + }); + + if (!lockResult) { + return; + } + + // Heartbeat to extend lock + const heartbeat = setInterval(async () => { + await redis.expire(LOCK_KEY, LOCK_TTL); + }, HEARTBEAT_INTERVAL); + + try { + await doExpensiveWork(); + } finally { + clearInterval(heartbeat); + await redis.del(LOCK_KEY); + } +} +``` + +## Batch Processing Patterns + +### Chunked Processing + +For large datasets, process in chunks: + +```typescript +const CHUNK_SIZE = 100; + +async function processAllOrders(orderIds: string[]): Promise<void> { + // Split into chunks + const chunks: string[][] = []; + for (let i = 0; i < orderIds.length; i += CHUNK_SIZE) { + chunks.push(orderIds.slice(i, i + CHUNK_SIZE)); + } + + logger.info("Processing orders in chunks", { + totalOrders: orderIds.length, + chunkCount: chunks.length, + chunkSize: CHUNK_SIZE, + }); + + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + if (!chunk) continue; + + await processOrderChunk(chunk); + + logger.info("Chunk processed", { + chunkIndex: i + 1, + totalChunks: chunks.length, + }); + } +} + +async function processOrderChunk(orderIds: string[]): Promise<void> { + // Batch database query + const orders = await db + .select() + .from(orderTable) + .where(inArray(orderTable.id, orderIds)); + + // Parallel processing with concurrency limit + const limiter = pLimit(10); + + await Promise.all( + orders.map(order => limiter(() => processOrder(order))) + ); +} +``` + +### Progress Reporting + +Track and report progress for long operations: + +```typescript +interface ProgressTracker { + total: number; + processed: number; + failed: number; + startTime: number; +} + +async function batchProcessWithProgress( + items: string[], + progressCallback?: (progress: ProgressTracker) => void +): Promise<void> { + const progress: ProgressTracker = { + total: items.length, + processed: 0, + failed: 0, + startTime: Date.now(), + }; + + const UPDATE_INTERVAL = 20; // Report every 20 items + + for (const item of items) { + try { + await processItem(item); + progress.processed++; + } catch { + progress.failed++; + } + + // Report progress periodically + if ((progress.processed + progress.failed) % UPDATE_INTERVAL === 0) { + progressCallback?.(progress); + + logger.info("Batch progress", { + processed: progress.processed, + failed: progress.failed, + total: progress.total, + elapsedMs: Date.now() - progress.startTime, + }); + } + } +} +``` + +## Memory Optimization + +### Streaming Large Datasets + +For very large datasets, use streaming: + +```typescript +async function* streamOrders(userId: string): AsyncGenerator<Order> { + let cursor: string | undefined; + const PAGE_SIZE = 100; + + while (true) { + const orders = await db + .select() + .from(orderTable) + .where(and( + eq(orderTable.userId, userId), + cursor ? gt(orderTable.id, cursor) : undefined + )) + .orderBy(asc(orderTable.id)) + .limit(PAGE_SIZE); + + if (orders.length === 0) { + break; + } + + for (const order of orders) { + yield order; + } + + const lastOrder = orders[orders.length - 1]; + cursor = lastOrder?.id; + + if (orders.length < PAGE_SIZE) { + break; + } + } +} + +// Usage +for await (const order of streamOrders(userId)) { + await processOrder(order); +} +``` diff --git a/.trellis/spec/backend/quality.md b/.trellis/spec/backend/quality.md new file mode 100644 index 0000000..dc00168 --- /dev/null +++ b/.trellis/spec/backend/quality.md @@ -0,0 +1,81 @@ +# Pre-commit Checklist + +Run through this checklist before committing backend code. + +## Type Safety + +- [ ] **No non-null assertions (`!`)** - Use local variables and conditionals for type narrowing +- [ ] **All API inputs have Zod schemas** - Defined in `types.ts` +- [ ] **All API outputs have Zod schemas** - Including `success` and `reason` fields +- [ ] **Enums imported from `@your-app/utils`** - Not from database package + +## Database Operations + +- [ ] **No `await` in loops** - Use `inArray` for batch queries +- [ ] **Batch inserts used** - Not individual inserts in loops +- [ ] **Conflict handling considered** - Use `onConflictDoUpdate` when appropriate +- [ ] **JSON columns cast properly** - `::jsonb` for jsonb functions +- [ ] **Raw SQL column names quoted** - Double quotes for camelCase columns + +## Logging + +- [ ] **No `console.log`** - Use `logger` from `@your-app/logs` +- [ ] **Structured logging used** - Pass objects, not string interpolation +- [ ] **Errors logged with context** - Include relevant IDs and stack traces +- [ ] **Sensitive data excluded** - No passwords, tokens, or PII in logs + +## Performance + +- [ ] **Parallel execution where possible** - Use `Promise.all` for independent operations +- [ ] **Concurrency control for external APIs** - Use `p-limit` for rate-limited APIs +- [ ] **Retry logic for rate limits** - Exponential backoff implemented +- [ ] **Caching considered** - For expensive or frequently accessed data + +## Error Handling + +- [ ] **Errors properly caught and logged** - With Sentry context when applicable +- [ ] **Appropriate error codes returned** - `NOT_FOUND`, `FORBIDDEN`, `BAD_REQUEST`, etc. +- [ ] **Batch operations handle partial failures** - Return detailed error information + +## Code Organization + +- [ ] **Code in correct location** - Procedures, lib, types in right directories +- [ ] **Reusable logic extracted** - Shared code in `lib/` directory +- [ ] **Naming conventions followed** - Schemas, types, functions named correctly + +## Quick Reference + +### Response Format +```typescript +return { + success: true, + reason: "Operation completed successfully", + // additional fields +}; +``` + +### Batch Query Pattern +```typescript +const items = await db + .select() + .from(itemTable) + .where(inArray(itemTable.parentId, parentIds)); + +const itemsByParent = groupBy(items, "parentId"); +``` + +### Logging Pattern +```typescript +logger.info("Operation completed", { + operationId, + userId, + itemCount: items.length, +}); +``` + +### Error Pattern +```typescript +if (!resource) { + throw new ORPCError("NOT_FOUND", { message: "Resource not found" }); +} +``` diff --git a/.trellis/spec/backend/type-safety.md b/.trellis/spec/backend/type-safety.md new file mode 100644 index 0000000..9167d57 --- /dev/null +++ b/.trellis/spec/backend/type-safety.md @@ -0,0 +1,316 @@ +# Type Safety Guidelines + +This document covers TypeScript best practices and type safety patterns for backend development. + +## Critical Rules + +### 1. NO Non-null Assertions (`!`) + +Never use the non-null assertion operator (`!`). It bypasses TypeScript's null checking and can lead to runtime errors. + +```typescript +// BAD - Non-null assertion +const user = users.find(u => u.id === id); +await processUser(user!); // Dangerous! + +// GOOD - Use local variable for type narrowing +const user = users.find(u => u.id === id); +if (!user) { + return { success: false, reason: "User not found" }; +} +// TypeScript now knows user is defined +await processUser(user); +``` + +**Why this matters:** + +- Non-null assertions (`!`) tell TypeScript to trust you, but runtime doesn't care +- If the value is actually `null` or `undefined`, you get a runtime crash +- Local variable narrowing is verifiable at both compile-time and runtime + +### 2. All Inputs/Outputs Must Have Zod Schemas + +Every API endpoint must define explicit input and output schemas using Zod. + +```typescript +// types.ts +import { z } from "zod"; + +// Input Schema +export const updateUserInputSchema = z.object({ + userId: z.string(), + name: z.string().min(1).max(100).optional(), + email: z.string().email().optional(), + settings: z.object({ + notifications: z.boolean(), + theme: z.enum(["light", "dark"]), + }).optional(), +}); + +// Output Schema +export const updateUserOutputSchema = z.object({ + success: z.boolean(), + reason: z.string(), + user: z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + }).optional(), +}); + +// Type exports (inferred from schemas) +export type UpdateUserInput = z.infer<typeof updateUserInputSchema>; +export type UpdateUserOutput = z.infer<typeof updateUserOutputSchema>; +``` + +**Using schemas in procedures:** + +```typescript +// procedures/update.ts +export const updateUser = protectedProcedure + .route({ + method: "PATCH", + path: "/users/:userId", + tags: ["Users"], + summary: "Update user profile", + }) + .input(updateUserInputSchema) + .output(updateUserOutputSchema) + .handler(async ({ input, context }) => { + // input is fully typed as UpdateUserInput + const { userId, name, email, settings } = input; + + // ... implementation + + return { + success: true, + reason: "User updated successfully", + user: { id: userId, name: updatedName, email: updatedEmail }, + }; + }); +``` + +### 3. Import Enums from Shared Utils Package + +**Never import enums directly from the database package.** The database package includes the PostgreSQL client, which can cause issues in certain environments (edge runtime, client-side code). + +```typescript +// BAD - Imports database client as side effect +import { messageCategoryEnum } from "@your-app/database/drizzle/schema/postgres"; + +// GOOD - Import from utils (no database client dependency) +import { + messageCategoryZodSchema, + type MessageCategory, + MESSAGE_CATEGORY_VALUES, +} from "@your-app/utils"; +``` + +**How enums are organized in the utils package:** + +```typescript +// packages/utils/lib/enum-types.ts +import { z } from "zod"; + +// Import enum values from schema (not the full database package) +import { + statusEnum +} from "@your-app/database/drizzle/schema"; + +// Export as Zod schema and TypeScript type +export const ORDER_STATUS_VALUES = statusEnum.enumValues; +export const orderStatusZodSchema = z.enum(ORDER_STATUS_VALUES); +export type OrderStatus = z.infer<typeof orderStatusZodSchema>; +``` + +### 4. Standard Response Format + +All API responses must include `success` and `reason` fields for consistent error handling. + +```typescript +// Output schema pattern +export const operationResultSchema = z.object({ + success: z.boolean(), + reason: z.string(), + // Additional fields as needed + data: z.unknown().optional(), +}); + +// Success response +return { + success: true, + reason: "Operation completed successfully", + data: result, +}; + +// Error response +return { + success: false, + reason: "Insufficient permissions to perform this action", +}; +``` + +**Batch operation response pattern:** + +```typescript +export const batchOperationResultSchema = z.object({ + success: z.boolean(), + total: z.number(), + processed: z.number(), + failed: z.number(), + errors: z.array(z.object({ + itemId: z.string(), + error: z.string(), + })).optional(), +}); +``` + +## Type Narrowing Patterns + +### Array Operations + +```typescript +// BAD - Assumes array has elements +const firstOrder = orders[0]; +await processOrder(firstOrder!); + +// GOOD - Check first +const firstOrder = orders[0]; +if (!firstOrder) { + return { success: false, reason: "No orders found" }; +} +await processOrder(firstOrder); +``` + +### Optional Chaining with Fallback + +```typescript +// BAD - Non-null assertion on optional property +const userName = user.profile!.name!; + +// GOOD - Safe access with fallback +const userName = user.profile?.name ?? "Unknown"; + +// GOOD - When value is required, validate first +const profile = user.profile; +if (!profile?.name) { + throw new ORPCError("BAD_REQUEST", { message: "Profile name is required" }); +} +const userName = profile.name; +``` + +### Map/Find Operations + +```typescript +// BAD - Assuming find always succeeds +const account = accounts.find(a => a.id === accountId)!; + +// GOOD - Handle the undefined case +const account = accounts.find(a => a.id === accountId); +if (!account) { + throw new ORPCError("NOT_FOUND", { message: "Account not found" }); +} +// account is now guaranteed to be defined +``` + +## Zod Schema Best Practices + +### Reusable Base Schemas + +```typescript +// Define reusable schemas +const paginationSchema = z.object({ + page: z.number().min(1).default(1), + limit: z.number().min(1).max(100).default(20), +}); + +const timestampSchema = z.object({ + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}); + +// Compose into larger schemas +export const listOrdersInputSchema = paginationSchema.extend({ + status: orderStatusZodSchema.optional(), + customerId: z.string().optional(), +}); + +export const orderSchema = z.object({ + id: z.string(), + status: orderStatusZodSchema, + total: z.number(), +}).merge(timestampSchema); +``` + +### Discriminated Unions + +```typescript +// For polymorphic responses +export const notificationSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("email"), + recipient: z.string().email(), + subject: z.string(), + }), + z.object({ + type: z.literal("sms"), + phoneNumber: z.string(), + message: z.string(), + }), + z.object({ + type: z.literal("push"), + deviceToken: z.string(), + title: z.string(), + body: z.string(), + }), +]); +``` + +### Transform and Refine + +```typescript +// Transform input data +export const createProductInputSchema = z.object({ + name: z.string().transform(s => s.trim()), + price: z.string().transform(s => parseFloat(s)), + tags: z.string().transform(s => s.split(",").map(t => t.trim())), +}); + +// Add custom validation +export const dateRangeSchema = z.object({ + startDate: z.string().datetime(), + endDate: z.string().datetime(), +}).refine( + data => new Date(data.endDate) > new Date(data.startDate), + { message: "End date must be after start date" } +); +``` + +## Error Handling Types + +Use typed errors with oRPC: + +```typescript +import { ORPCError } from "@orpc/server"; + +// Standard error codes +throw new ORPCError("NOT_FOUND", { message: "Resource not found" }); +throw new ORPCError("FORBIDDEN", { message: "Access denied" }); +throw new ORPCError("BAD_REQUEST", { message: "Invalid input" }); +throw new ORPCError("UNAUTHORIZED", { message: "Authentication required" }); +throw new ORPCError("INTERNAL_SERVER_ERROR", { message: "Unexpected error" }); +``` + +## Type Inference Helpers + +```typescript +// Infer types from Drizzle tables +type User = typeof userTable.$inferSelect; +type NewUser = typeof userTable.$inferInsert; + +// Infer from Zod schemas +type CreateOrderInput = z.infer<typeof createOrderInputSchema>; + +// Utility types for partial updates +type UpdateOrderInput = Partial<Omit<CreateOrderInput, "id">>; +``` diff --git a/.trellis/spec/big-question/index.md b/.trellis/spec/big-question/index.md new file mode 100644 index 0000000..b0f6566 --- /dev/null +++ b/.trellis/spec/big-question/index.md @@ -0,0 +1,34 @@ +# Common Issues and Solutions + +> Documented pitfalls discovered while building production Next.js fullstack applications. +> These issues apply to any project using Next.js with PostgreSQL, Drizzle ORM, Tailwind CSS, and i18n. + +## Severity Levels + +| Level | Description | +| -------- | ---------------------------------------------------- | +| Critical | Build fails or data corruption | +| Warning | Degraded experience, workaround exists | +| Info | Minor visual issue, easy to fix once identified | + +--- + +## Issue Index + +| Issue | Category | Severity | +| ---------------------------------------------------------------- | ---------------- | -------- | +| [postgres-json-jsonb.md](./postgres-json-jsonb.md) | Database/ORM | Critical | +| [sentry-nextintl-conflict.md](./sentry-nextintl-conflict.md) | Plugin Conflicts | Critical | +| [turbopack-webpack-flexbox.md](./turbopack-webpack-flexbox.md) | Build System | Warning | +| [webkit-tap-highlight.md](./webkit-tap-highlight.md) | Mobile/CSS | Info | + +--- + +## How to Contribute + +Found a new pitfall? Add it to this directory: + +1. Create a new `.md` file with a descriptive kebab-case name +2. Follow the existing format: Problem, Root Cause, Solution, Key Takeaways +3. Update this index table with the correct category and severity +4. Include reproducible code examples whenever possible diff --git a/.trellis/spec/big-question/postgres-json-jsonb.md b/.trellis/spec/big-question/postgres-json-jsonb.md new file mode 100644 index 0000000..be71fc9 --- /dev/null +++ b/.trellis/spec/big-question/postgres-json-jsonb.md @@ -0,0 +1,192 @@ +# PostgreSQL JSON vs JSONB Type Issues with Drizzle ORM + +## Problem + +Database queries using PostgreSQL's `jsonb_*` functions fail with type errors, even though the column appears to store JSON data correctly. + +**Error message:** +``` +function jsonb_array_elements(json) does not exist +HINT: No function matches the given name and argument types. +You might need to add explicit type casts. +``` + +**Additional symptoms:** +- Raw SQL queries fail to find columns with camelCase names +- JSON aggregation functions return unexpected results + +## Root Cause + +### Issue 1: Drizzle's `json()` Maps to PostgreSQL `json`, Not `jsonb` + +When defining a JSON column in Drizzle ORM: + +```typescript +// Drizzle schema definition +export const orders = pgTable("orders", { + id: text("id").primaryKey(), + metadata: json("metadata"), // Creates PostgreSQL 'json' type, NOT 'jsonb' +}); +``` + +PostgreSQL has two JSON types with different characteristics: + +| Feature | `json` | `jsonb` | +|---------|--------|---------| +| Storage | Text (preserves whitespace, key order) | Binary (normalized) | +| Functions | `json_*` functions only | `jsonb_*` functions only | +| Indexing | Limited | GIN indexes supported | +| Performance | Slower for operations | Faster for operations | + +The `jsonb_*` functions (like `jsonb_array_elements`, `jsonb_extract_path`) **only work with `jsonb` type**. + +### Issue 2: Column Name Case Sensitivity + +PostgreSQL treats unquoted identifiers as lowercase. If your column uses camelCase: + +```sql +-- This fails (looks for column named 'metadata' in lowercase) +SELECT metadata->>'userId' FROM orders; + +-- Column is actually named "metaData" with exact case +SELECT "metaData"->>'userId' FROM orders; +``` + +## Solution + +### Solution 1: Use Type Cast for jsonb Functions + +Add `::jsonb` type cast before using jsonb functions: + +```typescript +// Before (fails) +const result = await db.execute(sql` + SELECT jsonb_array_elements(items) as item + FROM orders + WHERE id = ${orderId} +`); + +// After (works) +const result = await db.execute(sql` + SELECT jsonb_array_elements(items::jsonb) as item + FROM orders + WHERE id = ${orderId} +`); +``` + +### Solution 2: Define Column as `jsonb` in Schema + +If you need jsonb functionality frequently, define the column as jsonb: + +```typescript +import { pgTable, text, jsonb } from "drizzle-orm/pg-core"; + +export const orders = pgTable("orders", { + id: text("id").primaryKey(), + metadata: jsonb("metadata"), // Now uses PostgreSQL 'jsonb' type +}); +``` + +**Note:** This requires a migration if the column already exists. + +### Solution 3: Quote camelCase Column Names in Raw SQL + +Always use double quotes for camelCase column names: + +```typescript +// Before (fails - column not found) +const result = await db.execute(sql` + SELECT "userId", createdAt + FROM orders +`); + +// After (works) +const result = await db.execute(sql` + SELECT "userId", "createdAt" + FROM orders +`); +``` + +### Complete Example: Querying JSON Array Data + +```typescript +import { sql } from "drizzle-orm"; + +// Table with json column storing an array of items +// items: [{ "productId": "123", "quantity": 2 }, ...] + +// Query to find orders containing a specific product +async function findOrdersWithProduct(productId: string) { + const result = await db.execute(sql` + SELECT + o.id, + o."createdAt", + item->>'productId' as "productId", + (item->>'quantity')::int as quantity + FROM orders o, + jsonb_array_elements(o.items::jsonb) as item + WHERE item->>'productId' = ${productId} + `); + + return result.rows; +} + +// Query to aggregate JSON array data +async function getOrderItemStats(orderId: string) { + const result = await db.execute(sql` + SELECT + COUNT(*) as "itemCount", + SUM((item->>'quantity')::int) as "totalQuantity" + FROM orders o, + jsonb_array_elements(o.items::jsonb) as item + WHERE o.id = ${orderId} + `); + + return result.rows[0]; +} +``` + +### Best Practice: Create a Helper for JSON Queries + +```typescript +// utils/db-helpers.ts +import { sql, SQL } from "drizzle-orm"; + +/** + * Wraps a column reference with ::jsonb cast for use with jsonb functions + */ +export function asJsonb(column: SQL | string): SQL { + if (typeof column === "string") { + return sql.raw(`"${column}"::jsonb`); + } + return sql`${column}::jsonb`; +} + +// Usage +const result = await db.execute(sql` + SELECT jsonb_array_elements(${asJsonb("items")}) as item + FROM orders +`); +``` + +## Key Takeaways + +1. **Know the difference between `json` and `jsonb`** + - `json`: Text storage, use `json_*` functions + - `jsonb`: Binary storage, use `jsonb_*` functions, better performance + +2. **Drizzle's `json()` creates PostgreSQL `json` type** - use `jsonb()` if you need jsonb functionality + +3. **Always add `::jsonb` cast** when using jsonb functions with json columns + +4. **Quote camelCase identifiers** in raw SQL queries with double quotes + +5. **Prefer Drizzle's query builder** over raw SQL when possible to avoid these issues + +6. **Test raw SQL queries** directly in a PostgreSQL client before using in code + +## Related Resources + +- [PostgreSQL JSON Types Documentation](https://www.postgresql.org/docs/current/datatype-json.html) +- [PostgreSQL JSON Functions](https://www.postgresql.org/docs/current/functions-json.html) +- [Drizzle ORM PostgreSQL Column Types](https://orm.drizzle.team/docs/column-types/pg) diff --git a/.trellis/spec/big-question/sentry-nextintl-conflict.md b/.trellis/spec/big-question/sentry-nextintl-conflict.md new file mode 100644 index 0000000..12347a8 --- /dev/null +++ b/.trellis/spec/big-question/sentry-nextintl-conflict.md @@ -0,0 +1,223 @@ +# Sentry and next-intl Build Configuration Conflict + +## Problem + +Production builds fail with the error: + +``` +Error: Couldn't find next-intl config file +``` + +or + +``` +Error: Failed to collect page data for /[locale]/page +``` + +The build works fine in development mode but fails during `next build`. + +## Root Cause + +The `withSentryConfig` wrapper in `next.config.js` interferes with other Next.js plugins, particularly `next-intl`'s plugin (`createNextIntlPlugin`). + +### Technical Details + +When you chain multiple config wrappers: + +```javascript +// next.config.js - Problematic configuration +import { withSentryConfig } from "@sentry/nextjs"; +import createNextIntlPlugin from "next-intl/plugin"; + +const withNextIntl = createNextIntlPlugin(); + +const nextConfig = { + // your config +}; + +// This chaining causes conflicts +export default withSentryConfig(withNextIntl(nextConfig), { + // Sentry options +}); +``` + +The issue occurs because: + +1. **Plugin execution order matters** - Sentry's wrapper modifies the webpack configuration in ways that can break other plugins' assumptions +2. **Build-time vs runtime** - Some plugins expect to run at specific build phases +3. **Config mutation** - Wrappers may mutate the config object in incompatible ways + +Specifically, `withSentryConfig`: +- Modifies webpack configuration extensively +- Adds custom loaders and plugins +- May interfere with `next-intl`'s message loading mechanism + +## Solution + +### Solution 1: Remove withSentryConfig Wrapper (Recommended) + +Sentry's runtime features still work via `instrumentation.ts` without the config wrapper: + +```javascript +// next.config.js - Fixed configuration +import createNextIntlPlugin from "next-intl/plugin"; + +const withNextIntl = createNextIntlPlugin(); + +const nextConfig = { + // your config +}; + +// Only use next-intl wrapper, no Sentry wrapper +export default withNextIntl(nextConfig); +``` + +**Why Sentry still works:** + +The `withSentryConfig` wrapper is primarily for: +- Source map uploading +- Build-time instrumentation +- Release management + +However, Sentry's core error tracking works through `instrumentation.ts`: + +```typescript +// instrumentation.ts +import * as Sentry from "@sentry/nextjs"; + +export function register() { + if (process.env.NEXT_RUNTIME === "nodejs") { + Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1.0, + // ... other options + }); + } + + if (process.env.NEXT_RUNTIME === "edge") { + Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1.0, + }); + } +} +``` + +### Solution 2: Use Sentry Without Source Maps + +If you need some Sentry build features but want to avoid conflicts: + +```javascript +// next.config.js +import { withSentryConfig } from "@sentry/nextjs"; +import createNextIntlPlugin from "next-intl/plugin"; + +const withNextIntl = createNextIntlPlugin(); + +const nextConfig = { + // your config +}; + +// Apply next-intl first +const configWithIntl = withNextIntl(nextConfig); + +// Conditionally apply Sentry only if not causing issues +const finalConfig = process.env.SKIP_SENTRY_BUILD + ? configWithIntl + : withSentryConfig(configWithIntl, { + silent: true, + disableSourceMapUpload: true, // Disable problematic feature + }); + +export default finalConfig; +``` + +### Solution 3: Alternative Plugin Order + +Sometimes reversing the wrapper order helps: + +```javascript +// Try applying Sentry first, then next-intl +const configWithSentry = withSentryConfig(nextConfig, sentryOptions); +export default withNextIntl(configWithSentry); +``` + +**Note:** This may or may not work depending on your specific versions. + +### Solution 4: Separate Sentry Configuration + +Use Sentry CLI for source map upload instead of the webpack plugin: + +```bash +# In your CI/CD pipeline after build +npx @sentry/cli sourcemaps upload ./next/static --org your-org --project your-project +``` + +```javascript +// next.config.js - Clean configuration +import createNextIntlPlugin from "next-intl/plugin"; + +const withNextIntl = createNextIntlPlugin(); + +const nextConfig = { + productionBrowserSourceMaps: true, // Enable source maps for Sentry CLI +}; + +export default withNextIntl(nextConfig); +``` + +## Verification Steps + +After applying the fix: + +1. **Clean build artifacts:** + ```bash + rm -rf .next node_modules/.cache + ``` + +2. **Test production build:** + ```bash + pnpm build + ``` + +3. **Verify Sentry works in production:** + ```bash + pnpm start + # Trigger a test error and check Sentry dashboard + ``` + +4. **Test i18n routing:** + ```bash + # Visit different locale routes + curl http://localhost:3000/en/page + curl http://localhost:3000/de/page + ``` + +## Key Takeaways + +1. **Plugin wrappers can conflict** - Be cautious when combining multiple Next.js config wrappers + +2. **Sentry works without withSentryConfig** - Core error tracking functions via `instrumentation.ts` + +3. **Order matters** - Try different wrapper orders if you must use multiple plugins + +4. **Source maps are optional** - You can still get stack traces without source map upload + +5. **Test builds locally** - Always run `pnpm build` before deploying + +6. **Keep dependencies updated** - Plugin compatibility issues are often fixed in newer versions + +## Version Information + +This issue was observed with: +- Next.js 14.x / 15.x +- @sentry/nextjs 7.x / 8.x +- next-intl 3.x + +Check the respective changelogs for compatibility updates. + +## Related Resources + +- [Sentry Next.js SDK Documentation](https://docs.sentry.io/platforms/javascript/guides/nextjs/) +- [next-intl Plugin Documentation](https://next-intl-docs.vercel.app/docs/getting-started/app-router) +- [Next.js Configuration](https://nextjs.org/docs/app/api-reference/next-config-js) diff --git a/.trellis/spec/big-question/turbopack-webpack-flexbox.md b/.trellis/spec/big-question/turbopack-webpack-flexbox.md new file mode 100644 index 0000000..a7a69ff --- /dev/null +++ b/.trellis/spec/big-question/turbopack-webpack-flexbox.md @@ -0,0 +1,141 @@ +# Turbopack vs Webpack Flexbox Layout Differences + +## Problem + +Layout works correctly in development mode (Turbopack) but breaks in production (Webpack). Specifically, flex containers and their children behave differently between the two bundlers. + +**Symptoms:** +- Components have correct height in dev mode but collapse or overflow in production +- Scrollable areas work in dev but fail in prod +- Nested flex layouts display differently between environments + +## Root Cause + +Turbopack (Next.js dev mode default) and Webpack (production build) have subtle differences in how they process CSS, particularly regarding flexbox behavior: + +1. **Turbopack is stricter** about explicit flexbox properties +2. **Webpack may auto-infer** certain flex child behaviors that Turbopack does not +3. The difference lies in how CSS is compiled and applied, not in the CSS specification itself + +### Technical Details + +When a flex container has `flex-direction: column` and children that need to fill available space, the behavior depends on: + +- The `align-items` property (defaults to `stretch` but may not be consistently applied) +- Whether children have explicit `height` or `flex` properties +- The interaction between nested flex containers + +**Example of problematic layout:** + +```tsx +// Parent component +<div className="flex flex-col h-screen"> + <Header /> {/* Fixed height */} + <main className="flex-1 flex"> {/* Should fill remaining space */} + <Sidebar /> + <Content /> {/* Should scroll internally */} + </main> +</div> +``` + +In Turbopack, the `main` element might not properly pass its height to children without explicit `items-stretch`. + +## Solution + +### 1. Explicitly Set `items-stretch` on Flex Containers + +Add `items-stretch` to main flex containers that need children to fill available space: + +```tsx +// Before (inconsistent between Turbopack/Webpack) +<div className="flex flex-col h-screen"> + <main className="flex-1 flex"> + {/* children */} + </main> +</div> + +// After (consistent behavior) +<div className="flex flex-col h-screen items-stretch"> + <main className="flex-1 flex items-stretch"> + {/* children */} + </main> +</div> +``` + +### 2. Apply Parent/Child Responsibility Separation + +Follow a clear pattern for layout responsibilities: + +**Parent's Responsibility:** +- Define the flex container (`flex`, `flex-col`, `flex-row`) +- Set alignment (`items-stretch`, `justify-between`) +- Control overall dimensions (`h-screen`, `w-full`) + +**Child's Responsibility:** +- Define its own flex behavior (`flex-1`, `flex-shrink-0`) +- Handle internal overflow (`overflow-auto`, `overflow-hidden`) +- Set min/max constraints (`min-h-0`, `max-w-full`) + +### 3. Use `min-h-0` for Scrollable Flex Children + +When a flex child needs internal scrolling: + +```tsx +<div className="flex flex-col h-full items-stretch"> + <div className="flex-shrink-0">Fixed Header</div> + <div className="flex-1 min-h-0 overflow-auto"> + {/* Scrollable content */} + </div> +</div> +``` + +The `min-h-0` is crucial because flex items default to `min-height: auto`, which can prevent overflow from working correctly. + +### Complete Example + +```tsx +// App layout with consistent dev/prod behavior +function AppLayout({ children }: { children: React.ReactNode }) { + return ( + <div className="flex flex-col h-screen items-stretch"> + {/* Fixed navigation */} + <nav className="flex-shrink-0 h-16 border-b"> + <Navigation /> + </nav> + + {/* Main content area */} + <div className="flex-1 flex items-stretch min-h-0"> + {/* Sidebar */} + <aside className="w-64 flex-shrink-0 border-r overflow-auto"> + <SidebarContent /> + </aside> + + {/* Main content with internal scroll */} + <main className="flex-1 min-w-0 overflow-auto"> + {children} + </main> + </div> + </div> + ); +} +``` + +## Key Takeaways + +1. **Always test production builds locally** before deployment using `pnpm build && pnpm start` + +2. **Be explicit with flexbox properties** - Don't rely on browser defaults or bundler behavior + +3. **Use `items-stretch` explicitly** on containers where children need to fill space + +4. **Remember `min-h-0` and `min-w-0`** for scrollable flex children + +5. **Separate layout responsibilities** between parent (container behavior) and child (self behavior) + +6. **Document layout patterns** in your project to ensure consistency across the team + +## Related Resources + +- [CSS Flexbox Guide](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) +- [Next.js Turbopack Documentation](https://nextjs.org/docs/architecture/turbopack) +- [Tailwind CSS Flexbox Utilities](https://tailwindcss.com/docs/flex) diff --git a/.trellis/spec/big-question/webkit-tap-highlight.md b/.trellis/spec/big-question/webkit-tap-highlight.md new file mode 100644 index 0000000..37f0dd9 --- /dev/null +++ b/.trellis/spec/big-question/webkit-tap-highlight.md @@ -0,0 +1,218 @@ +# WebKit Tap Highlight and Border-Radius Issues on Mobile + +## Problem + +Buttons and interactive elements lose their `border-radius` styling when tapped on mobile devices (iOS Safari, Chrome on iOS). The element briefly shows a rectangular highlight instead of respecting the rounded corners. + +**Symptoms:** +- Button appears with sharp corners during tap/touch +- A blue or gray rectangular overlay flashes on touch +- The visual glitch only occurs on WebKit-based mobile browsers +- Desktop browsers and Android Chrome don't show the issue + +## Root Cause + +WebKit browsers apply a default tap highlight effect to interactive elements. This highlight: + +1. **Ignores `border-radius`** - The highlight is applied as a simple rectangular overlay +2. **Uses system default color** - Typically a semi-transparent blue or gray +3. **Overrides visual styling** - The highlight appears on top of your custom styles + +### Technical Details + +When you tap an element on iOS Safari: + +```css +/* WebKit's default behavior (pseudo-representation) */ +element:active { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1); + /* This creates a RECTANGULAR overlay, ignoring border-radius */ +} +``` + +The tap highlight is rendered as a separate layer that doesn't respect the element's `border-radius`, `clip-path`, or other shape-defining properties. + +## Solution + +### Solution 1: Disable Tap Highlight + Wrapper with Overflow Hidden + +The most reliable solution combines two techniques: + +```tsx +// Button component with proper mobile touch handling +function Button({ children, className, ...props }: ButtonProps) { + return ( + <div className="rounded-lg overflow-hidden inline-block"> + <button + className={cn("rounded-lg px-4 py-2 bg-blue-500 text-white", className)} + style={{ WebkitTapHighlightColor: "transparent" }} + {...props} + > + {children} + </button> + </div> + ); +} +``` + +**Why this works:** +1. `WebkitTapHighlightColor: "transparent"` removes the default highlight +2. The wrapper `div` with `overflow-hidden` clips any remaining visual artifacts +3. Both elements have matching `border-radius` for consistent appearance + +### Solution 2: CSS-Only Approach + +If you can't modify the component structure: + +```css +/* In your global CSS */ +.tap-safe { + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; +} + +/* Custom active state to replace the highlight */ +.tap-safe:active { + opacity: 0.8; + transform: scale(0.98); +} +``` + +```tsx +<button className="tap-safe rounded-lg px-4 py-2 bg-blue-500"> + Click me +</button> +``` + +### Solution 3: Tailwind CSS Utility Class + +Add a reusable utility in your Tailwind config: + +```javascript +// tailwind.config.js +module.exports = { + theme: { + extend: {}, + }, + plugins: [ + function({ addUtilities }) { + addUtilities({ + '.tap-highlight-none': { + '-webkit-tap-highlight-color': 'transparent', + }, + }); + }, + ], +}; +``` + +Then use it in components: + +```tsx +<button className="tap-highlight-none rounded-lg px-4 py-2"> + Click me +</button> +``` + +### Solution 4: Wrapper Component for Consistent Behavior + +Create a reusable wrapper for all interactive rounded elements: + +```tsx +// components/ui/touch-safe-wrapper.tsx +interface TouchSafeWrapperProps { + children: React.ReactNode; + className?: string; + borderRadius?: string; +} + +export function TouchSafeWrapper({ + children, + className, + borderRadius = "rounded-lg" +}: TouchSafeWrapperProps) { + return ( + <div className={cn(borderRadius, "overflow-hidden inline-flex", className)}> + {children} + </div> + ); +} + +// Usage +<TouchSafeWrapper> + <button + className="rounded-lg px-4 py-2 bg-blue-500" + style={{ WebkitTapHighlightColor: "transparent" }} + > + Click me + </button> +</TouchSafeWrapper> +``` + +### Complete Example: Card with Clickable Areas + +```tsx +function ProductCard({ product }: { product: Product }) { + return ( + <div className="rounded-xl border p-4"> + <h3>{product.name}</h3> + <p>{product.description}</p> + + {/* Action buttons with tap-safe handling */} + <div className="flex gap-2 mt-4"> + <div className="rounded-lg overflow-hidden"> + <button + className="rounded-lg px-4 py-2 bg-blue-500 text-white" + style={{ WebkitTapHighlightColor: "transparent" }} + onClick={() => addToCart(product)} + > + Add to Cart + </button> + </div> + + <div className="rounded-lg overflow-hidden"> + <button + className="rounded-lg px-4 py-2 border border-gray-300" + style={{ WebkitTapHighlightColor: "transparent" }} + onClick={() => viewDetails(product)} + > + Details + </button> + </div> + </div> + </div> + ); +} +``` + +## Key Takeaways + +1. **WebKit tap highlight ignores border-radius** - This is browser behavior, not a CSS bug + +2. **Always set `WebkitTapHighlightColor: "transparent"`** on interactive elements with rounded corners + +3. **Use a wrapper with `overflow-hidden`** for the most reliable visual clipping + +4. **Test on actual iOS devices** - Simulators and browser dev tools may not reproduce the issue + +5. **Consider adding custom active states** to replace the removed tap feedback for better UX + +6. **Create reusable components** that handle mobile touch behavior consistently + +## Browser Support Notes + +| Browser | Needs Fix | +|---------|-----------| +| iOS Safari | Yes | +| Chrome on iOS | Yes (uses WebKit) | +| Firefox on iOS | Yes (uses WebKit) | +| Android Chrome | Usually no | +| Desktop browsers | No | + +## Related Resources + +- [MDN: -webkit-tap-highlight-color](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color) +- [WebKit Bug Tracker](https://bugs.webkit.org/) +- [CSS Tricks: Handling Touch Events](https://css-tricks.com/snippets/css/remove-gray-highlight-when-tapping-links-in-mobile-safari/) diff --git a/.trellis/spec/frontend/ai-sdk-integration.md b/.trellis/spec/frontend/ai-sdk-integration.md new file mode 100644 index 0000000..afec1ba --- /dev/null +++ b/.trellis/spec/frontend/ai-sdk-integration.md @@ -0,0 +1,255 @@ +# AI SDK Frontend Integration + +## 1. Overview + +This guide covers frontend integration with the Vercel AI SDK using `@ai-sdk/react`. Key topics include: + +- Using `@ai-sdk/react` for React integration +- Streaming chat with the `useChat` hook +- Tool call handling with proper format detection + +## 2. Basic Chat with useChat + +The `useChat` hook provides a simple interface for chat functionality: + +```typescript +"use client"; + +import { useChat } from "@ai-sdk/react"; + +export function ChatPanel() { + const { messages, input, handleInputChange, handleSubmit, status } = useChat({ + api: "/api/chat", + }); + + return ( + <div> + {messages.map((message) => ( + <div key={message.id}> + <strong>{message.role}:</strong> {message.content} + </div> + ))} + + <form onSubmit={handleSubmit}> + <input + value={input} + onChange={handleInputChange} + placeholder="Type a message..." + disabled={status === "streaming"} + /> + <button type="submit" disabled={status === "streaming"}> + Send + </button> + </form> + </div> + ); +} +``` + +## 3. Custom Transport with oRPC + +When using oRPC instead of standard fetch: + +```typescript +import { useChat } from "@ai-sdk/react"; +import { eventIteratorToStream } from "@orpc/client"; +import { orpcClient } from "@/lib/orpc-client"; + +export function ChatPanel({ sessionId }: { sessionId: string }) { + const { messages, sendMessage, status } = useChat({ + id: sessionId, + transport: { + async sendMessages(options) { + return eventIteratorToStream( + await orpcClient.chat.send( + { + sessionId, + messages: options.messages, + }, + { signal: options.abortSignal } + ) + ); + }, + reconnectToStream() { + throw new Error("Reconnect not supported"); + }, + }, + }); + + // ... rest of component +} +``` + +## 4. Tool Calls Handling + +**CRITICAL**: Tool calls have TWO different formats that must both be handled: + +### Format 1: Real-time Streaming + +During streaming, tool results appear as: + +```typescript +{ + type: "tool-createTask", // tool-{toolName} + toolCallId: "call_abc123", + state: "output-available", + input: { title: "...", priority: "high" }, + output: { success: true, taskId: "task_xyz" } // Direct object +} +``` + +### Format 2: History Restore + +When loading from history/database: + +```typescript +{ + type: "tool-result", + toolName: "createTask", + toolCallId: "call_abc123", + output: { + type: "json", + value: { success: true, taskId: "task_xyz" } // Nested in value + } +} +``` + +### Unified Handling Pattern + +```typescript +import { useChat } from "@ai-sdk/react"; +import { useEffect, useState, useRef } from "react"; + +export function AssistantPanel({ sessionId }: { sessionId: string }) { + const [createdItems, setCreatedItems] = useState<Map<string, CreatedItem>>(new Map()); + const toolCallsRef = useRef<Map<string, string>>(new Map()); + + const { messages, status } = useChat({ + id: sessionId, + transport: { /* ... */ }, + + // Handle real-time tool results + onData: (dataPart) => { + const payload = typeof dataPart === "object" && "json" in dataPart + ? (dataPart as { json: unknown }).json + : dataPart; + + if (typeof payload === "object" && payload !== null && "type" in payload) { + const { type, data } = payload as { type: string; data: any }; + + if (type === "tool-output-available" || type === "tool-result") { + const { toolCallId, output } = data; + const toolName = toolCallsRef.current.get(toolCallId); + + if (toolName === "createTask" && output?.success) { + setCreatedItems((prev) => { + if (prev.has(toolCallId)) return prev; + return new Map(prev).set(toolCallId, { + id: output.taskId, + title: output.title, + }); + }); + } + } + } + }, + }); + + // Handle history restore + useEffect(() => { + messages.forEach((message) => { + if (message.role !== "assistant") return; + + const parts = (message as any).parts || []; + parts.forEach((part: any) => { + // Match both formats + const isRealTime = part.type === "tool-createTask" && part.state === "output-available"; + const isRestored = part.type === "tool-result" && part.toolName === "createTask"; + + if ((isRealTime || isRestored) && part.output) { + const key = part.toolCallId || message.id; + + // Extract output (handle nested structure) + const rawOutput = part.output; + const output = rawOutput?.type === "json" && rawOutput?.value + ? rawOutput.value + : rawOutput; + + if (output?.success) { + setCreatedItems((prev) => { + if (prev.has(key)) return prev; + return new Map(prev).set(key, { + id: output.taskId, + title: output.title, + }); + }); + } + } + }); + }); + }, [messages, status]); + + return ( + <div> + {messages.map((message) => ( + <MessageBubble key={message.id} message={message} /> + ))} + + {/* Display created items */} + {Array.from(createdItems.values()).map((item) => ( + <CreatedItemCard key={item.id} item={item} /> + ))} + </div> + ); +} +``` + +## 5. Tool Call State Lifecycle + +During streaming, tool parts go through these states: + +| State | Description | +|-------|-------------| +| `input-streaming` | Tool input is being generated | +| `input-available` | Complete input ready | +| `output-available` | Tool executed, result available | +| `output-error` | Tool execution failed | + +## 6. Displaying Thought Process + +Show users what the AI is "thinking": + +```typescript +const [thoughtSteps, setThoughtSteps] = useState<ThoughtStep[]>([]); + +// In onData handler +if (type === "tool-input-start" || type === "tool-call") { + const { toolCallId, toolName, input } = data; + toolCallsRef.current.set(toolCallId, toolName); + + setThoughtSteps((prev) => [ + ...prev, + { id: toolCallId, toolName, status: "pending", input }, + ]); +} + +if (type === "tool-output-available") { + setThoughtSteps((prev) => + prev.map((step) => + step.id === toolCallId + ? { ...step, status: "done", result: output } + : step + ) + ); +} +``` + +## 7. Best Practices Summary + +| Rule | Description | +|------|-------------| +| Handle both tool formats | Real-time and history restore | +| Use toolCallId as key | Correlate calls across formats | +| Use useRef for toolName mapping | Avoid React state timing issues | +| onData for real-time UI | useEffect for history restore | +| Show thought process | Better UX for tool-heavy flows | diff --git a/.trellis/spec/frontend/api-integration.md b/.trellis/spec/frontend/api-integration.md new file mode 100644 index 0000000..ba5696f --- /dev/null +++ b/.trellis/spec/frontend/api-integration.md @@ -0,0 +1,464 @@ +# API Integration + +This document covers API integration patterns including oRPC client usage, real-time communication, and AI streaming. + +## oRPC Client Usage + +### Client Setup + +```typescript +// lib/orpc.ts +import { createORPCClient } from '@your-app/api/client'; // Replace with your monorepo package path + +export const orpcClient = createORPCClient({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, +}); +``` + +### Basic API Calls + +```typescript +// Simple GET +const users = await orpcClient.users.list(); + +// GET with parameters +const user = await orpcClient.users.get({ id: userId }); + +// POST (create) +const newUser = await orpcClient.users.create({ + name: 'John Doe', + email: 'john@example.com', +}); + +// PUT/PATCH (update) +const updatedUser = await orpcClient.users.update({ + id: userId, + name: 'Jane Doe', +}); + +// DELETE +await orpcClient.users.delete({ id: userId }); +``` + +### With React Query + +```typescript +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { orpcClient } from '@/lib/orpc'; + +// Query +export function useUsers() { + return useQuery({ + queryKey: ['users'], + queryFn: () => orpcClient.users.list(), + }); +} + +// Mutation +export function useCreateUser() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: orpcClient.users.create, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }); + }, + }); +} +``` + +## Query Patterns + +### Pagination + +```typescript +interface PaginationParams { + page: number; + pageSize: number; +} + +export function usePaginatedOrders({ page, pageSize }: PaginationParams) { + return useQuery({ + queryKey: ['orders', { page, pageSize }], + queryFn: () => orpcClient.orders.list({ page, pageSize }), + placeholderData: (prev) => prev, // Keep previous data while fetching + }); +} +``` + +### Filtering and Sorting + +```typescript +interface OrderFilters { + status?: string; + customerId?: string; + sortBy?: 'createdAt' | 'total'; + sortOrder?: 'asc' | 'desc'; +} + +export function useFilteredOrders(filters: OrderFilters) { + return useQuery({ + queryKey: ['orders', filters], + queryFn: () => orpcClient.orders.list(filters), + }); +} +``` + +### Prefetching + +```typescript +export function useOrdersWithPrefetch() { + const queryClient = useQueryClient(); + + const query = useQuery({ + queryKey: ['orders', { page: 1 }], + queryFn: () => orpcClient.orders.list({ page: 1 }), + }); + + // Prefetch next page + useEffect(() => { + if (query.data?.hasNextPage) { + queryClient.prefetchQuery({ + queryKey: ['orders', { page: 2 }], + queryFn: () => orpcClient.orders.list({ page: 2 }), + }); + } + }, [query.data, queryClient]); + + return query; +} +``` + +## Real-time Communication + +### WebSocket with Ably + +```typescript +// lib/ably.ts +import Ably from 'ably'; + +export const ablyClient = new Ably.Realtime({ + authUrl: '/api/ably/auth', +}); + +// Hook for real-time updates +export function useRealtimeOrders() { + const queryClient = useQueryClient(); + + useEffect(() => { + const channel = ablyClient.channels.get('orders'); + + channel.subscribe('order:created', (message) => { + queryClient.invalidateQueries({ queryKey: ['orders'] }); + }); + + channel.subscribe('order:updated', (message) => { + const order = message.data; + queryClient.setQueryData(['orders', order.id], order); + }); + + return () => { + channel.unsubscribe(); + }; + }, [queryClient]); +} +``` + +### WebSocket Connection Management + +```typescript +export function useWebSocket(channelName: string) { + const [isConnected, setIsConnected] = useState(false); + const channelRef = useRef<Ably.RealtimeChannel | null>(null); + + useEffect(() => { + const channel = ablyClient.channels.get(channelName); + channelRef.current = channel; + + channel.on('attached', () => setIsConnected(true)); + channel.on('detached', () => setIsConnected(false)); + + return () => { + channel.detach(); + }; + }, [channelName]); + + const subscribe = useCallback( + (event: string, callback: (data: unknown) => void) => { + channelRef.current?.subscribe(event, (message) => { + callback(message.data); + }); + }, + [] + ); + + return { isConnected, subscribe }; +} +``` + +## SSE/Streaming for AI Chat + +### Basic SSE Pattern + +```typescript +export function useAIChat() { + const [messages, setMessages] = useState<Message[]>([]); + const [isStreaming, setIsStreaming] = useState(false); + + const sendMessage = useCallback(async (content: string) => { + setIsStreaming(true); + + // Add user message + setMessages((prev) => [ + ...prev, + { role: 'user', content }, + ]); + + // Create placeholder for assistant response + setMessages((prev) => [ + ...prev, + { role: 'assistant', content: '' }, + ]); + + try { + const response = await fetch('/api/ai/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: content }), + }); + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + while (reader) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + setMessages((prev) => { + const updated = [...prev]; + const lastMessage = updated[updated.length - 1]; + lastMessage.content += chunk; + return updated; + }); + } + } finally { + setIsStreaming(false); + } + }, []); + + return { messages, sendMessage, isStreaming }; +} +``` + +### Using Vercel AI SDK + +```typescript +import { useChat } from 'ai/react'; + +export function useAIChatWithSDK() { + const { + messages, + input, + handleInputChange, + handleSubmit, + isLoading, + error, + } = useChat({ + api: '/api/ai/chat', + onFinish: (message) => { + // Handle completed message + }, + }); + + return { + messages, + input, + handleInputChange, + handleSubmit, + isLoading, + error, + }; +} +``` + +## AI Tool Calls Handling + +AI responses may include tool calls (function calls). The format differs between real-time streaming and history restore. + +### Real-time Streaming Format + +During streaming, tool calls arrive incrementally: + +```typescript +interface StreamingToolCall { + type: 'tool-call'; + toolCallId: string; + toolName: string; + args: Record<string, unknown>; +} + +interface StreamingToolResult { + type: 'tool-result'; + toolCallId: string; + result: unknown; +} +``` + +### History Restore Format + +When loading chat history, tool calls are embedded in messages: + +```typescript +interface HistoryMessage { + role: 'assistant'; + content: string; + toolInvocations?: Array<{ + toolCallId: string; + toolName: string; + args: Record<string, unknown>; + result?: unknown; + state: 'pending' | 'result' | 'error'; + }>; +} +``` + +### Unified Handler Pattern + +```typescript +interface ToolCall { + id: string; + name: string; + args: Record<string, unknown>; + result?: unknown; + state: 'pending' | 'result' | 'error'; +} + +function normalizeToolCall( + data: StreamingToolCall | HistoryMessage['toolInvocations'][0] +): ToolCall { + // Handle streaming format + if ('type' in data && data.type === 'tool-call') { + return { + id: data.toolCallId, + name: data.toolName, + args: data.args, + state: 'pending', + }; + } + + // Handle history format + return { + id: data.toolCallId, + name: data.toolName, + args: data.args, + result: data.result, + state: data.state, + }; +} + +// Usage in component +function ToolCallDisplay({ toolCall }: { toolCall: ToolCall }) { + switch (toolCall.name) { + case 'searchProducts': + return <ProductSearchResult args={toolCall.args} result={toolCall.result} />; + case 'createOrder': + return <OrderCreationResult args={toolCall.args} result={toolCall.result} />; + default: + return <GenericToolResult toolCall={toolCall} />; + } +} +``` + +### Handling Tool Call States + +```typescript +export function useToolCallHandler() { + const [pendingToolCalls, setPendingToolCalls] = useState<Map<string, ToolCall>>( + new Map() + ); + + const handleStreamChunk = useCallback((chunk: unknown) => { + if (isToolCall(chunk)) { + setPendingToolCalls((prev) => { + const next = new Map(prev); + next.set(chunk.toolCallId, normalizeToolCall(chunk)); + return next; + }); + } + + if (isToolResult(chunk)) { + setPendingToolCalls((prev) => { + const next = new Map(prev); + const existing = next.get(chunk.toolCallId); + if (existing) { + next.set(chunk.toolCallId, { + ...existing, + result: chunk.result, + state: 'result', + }); + } + return next; + }); + } + }, []); + + return { pendingToolCalls, handleStreamChunk }; +} +``` + +## Error Handling + +### API Error Handling + +```typescript +import { isORPCError } from '@your-app/api/client'; // Replace with your monorepo package path + +export function useCreateOrder() { + const [error, setError] = useState<string | null>(null); + + const mutation = useMutation({ + mutationFn: orpcClient.orders.create, + onError: (err) => { + if (isORPCError(err)) { + switch (err.code) { + case 'UNAUTHORIZED': + setError('Please sign in to continue'); + break; + case 'VALIDATION_ERROR': + setError('Please check your input'); + break; + default: + setError('Something went wrong'); + } + } else { + setError('Network error. Please try again.'); + } + }, + }); + + return { ...mutation, error }; +} +``` + +### Retry Logic + +```typescript +export function useResilientQuery() { + return useQuery({ + queryKey: ['data'], + queryFn: () => orpcClient.data.get(), + retry: 3, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + }); +} +``` + +## Best Practices + +1. **Centralize API Client**: Keep oRPC client configuration in one place +2. **Use Query Keys Consistently**: Follow a hierarchical naming convention +3. **Handle Loading States**: Always show feedback during API calls +4. **Implement Error Boundaries**: Catch and display errors gracefully +5. **Optimize Real-time**: Unsubscribe from channels when components unmount +6. **Type Everything**: Leverage TypeScript for API response types diff --git a/.trellis/spec/frontend/authentication.md b/.trellis/spec/frontend/authentication.md new file mode 100644 index 0000000..2402298 --- /dev/null +++ b/.trellis/spec/frontend/authentication.md @@ -0,0 +1,748 @@ +# Frontend Authentication with better-auth + +This document provides guidelines for implementing client-side authentication using better-auth in a Next.js React application. + +## 1. Overview + +better-auth provides a comprehensive authentication solution for React applications with: + +- **Session Management**: Cookie-based sessions with automatic refresh +- **Multiple Auth Methods**: Password, magic link, OAuth, and passkeys +- **Type Safety**: Full TypeScript support with inferred types +- **Plugin Architecture**: Extensible through plugins (2FA, organizations, admin, etc.) + +### Key Concepts + +- **Auth Client**: The main interface for all authentication operations +- **Session Context**: React context for accessing session state across components +- **Middleware**: Server-side route protection before rendering + +## 2. Auth Client Setup + +### Creating the Auth Client + +Create a centralized auth client that can be imported throughout your application: + +```typescript +// packages/auth/client.ts +import { + adminClient, + inferAdditionalFields, + magicLinkClient, + organizationClient, + passkeyClient, + twoFactorClient, +} from "better-auth/client/plugins"; +import { createAuthClient } from "better-auth/react"; +import type { auth } from "."; + +export const authClient = createAuthClient({ + plugins: [ + inferAdditionalFields<typeof auth>(), + magicLinkClient(), + organizationClient(), + adminClient(), + passkeyClient(), + twoFactorClient(), + ], +}); + +export type AuthClientErrorCodes = typeof authClient.$ERROR_CODES & { + INVALID_INVITATION: string; +}; +``` + +### Configuration Options + +The auth client supports various plugins based on your needs: + +| Plugin | Purpose | +|--------|---------| +| `inferAdditionalFields` | Type inference for custom user fields | +| `magicLinkClient` | Passwordless email login | +| `organizationClient` | Multi-tenant organization support | +| `adminClient` | Admin user management | +| `passkeyClient` | WebAuthn/Passkey authentication | +| `twoFactorClient` | Two-factor authentication | + +## 3. Session Hook/Context + +### Session Context Definition + +Define the session context type and create the context: + +```typescript +// lib/session-context.ts +import type { Session } from "@your-app/auth"; // Replace with your monorepo package path +import React from "react"; + +export const SessionContext = React.createContext< + | { + session: Session["session"] | null; + user: Session["user"] | null; + loaded: boolean; + reloadSession: () => Promise<void>; + } + | undefined +>(undefined); +``` + +### Session Provider Component + +Wrap your application with a SessionProvider to manage session state: + +```typescript +// components/SessionProvider.tsx +"use client"; +import { authClient } from "@your-app/auth/client"; // Replace with your monorepo package path +import { useQueryClient } from "@tanstack/react-query"; +import { type ReactNode, useEffect, useState } from "react"; +import { SessionContext } from "../lib/session-context"; + +// Query key for session caching +export const sessionQueryKey = ["user", "session"] as const; + +// Custom hook for fetching session +export const useSessionQuery = () => { + return useQuery({ + queryKey: sessionQueryKey, + queryFn: async () => { + const { data, error } = await authClient.getSession({ + query: { + disableCookieCache: true, + }, + }); + + if (error) { + throw new Error(error.message || "Failed to fetch session"); + } + + return data; + }, + staleTime: Number.POSITIVE_INFINITY, + refetchOnWindowFocus: false, + retry: false, + }); +}; + +export function SessionProvider({ children }: { children: ReactNode }) { + const queryClient = useQueryClient(); + const { data: session } = useSessionQuery(); + const [loaded, setLoaded] = useState(!!session); + + useEffect(() => { + if (session && !loaded) { + setLoaded(true); + } + }, [session, loaded]); + + return ( + <SessionContext.Provider + value={{ + loaded, + session: session?.session ?? null, + user: session?.user ?? null, + reloadSession: async () => { + const { data: newSession, error } = await authClient.getSession({ + query: { + disableCookieCache: true, + }, + }); + + if (error) { + throw new Error(error.message || "Failed to fetch session"); + } + + queryClient.setQueryData(sessionQueryKey, () => newSession); + }, + }} + > + {children} + </SessionContext.Provider> + ); +} +``` + +### useSession Hook + +Create a convenient hook to access session data: + +```typescript +// hooks/use-session.ts +import { useContext } from "react"; +import { SessionContext } from "../lib/session-context"; + +export const useSession = () => { + const sessionContext = useContext(SessionContext); + + if (sessionContext === undefined) { + throw new Error("useSession must be used within SessionProvider"); + } + + return sessionContext; +}; +``` + +### Usage Example + +```typescript +function UserGreeting() { + const { user, loaded } = useSession(); + + if (!loaded) { + return <div>Loading...</div>; + } + + if (!user) { + return <div>Please log in</div>; + } + + return <div>Welcome, {user.name}!</div>; +} +``` + +## 4. Protected Routes + +### Middleware for Route Protection + +Use Next.js middleware to protect routes at the server level: + +```typescript +// middleware.ts +import { getSessionCookie } from "better-auth/cookies"; +import { type NextRequest, NextResponse } from "next/server"; +import { withQuery } from "ufo"; + +export default async function middleware(req: NextRequest) { + const { pathname, origin } = req.nextUrl; + const sessionCookie = getSessionCookie(req); + + // Protect /app routes + if (pathname.startsWith("/app")) { + if (!sessionCookie) { + return NextResponse.redirect( + new URL( + withQuery("/auth/login", { + redirectTo: pathname, + }), + origin, + ), + ); + } + + return NextResponse.next(); + } + + // Allow auth routes + if (pathname.startsWith("/auth")) { + return NextResponse.next(); + } + + return NextResponse.next(); +} + +export const config = { + matcher: [ + "/((?!api|_next/static|_next/image|favicon.ico).*)", + ], +}; +``` + +### Client-Side Route Protection + +For additional client-side protection, redirect authenticated users away from auth pages: + +```typescript +"use client"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import { useSession } from "@/hooks/use-session"; + +export function AuthGuard({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const { user, loaded } = useSession(); + const redirectPath = "/app/dashboard"; + + useEffect(() => { + if (loaded && user) { + router.replace(redirectPath); + } + }, [user, loaded, router]); + + if (!loaded) { + return <LoadingSpinner />; + } + + if (user) { + return null; // Will redirect + } + + return <>{children}</>; +} +``` + +### Loading States + +Always handle loading states to prevent flash of unauthorized content: + +```typescript +function ProtectedContent() { + const { user, loaded } = useSession(); + + // Show loading while session is being fetched + if (!loaded) { + return ( + <div className="flex items-center justify-center min-h-screen"> + <Spinner /> + </div> + ); + } + + // Redirect or show unauthorized message + if (!user) { + return <Redirect to="/auth/login" />; + } + + return <DashboardContent user={user} />; +} +``` + +## 5. Login/Logout Flows + +### Email/Password Sign In + +```typescript +"use client"; +import { authClient } from "@your-app/auth/client"; // Replace with your monorepo package path +import { useRouter } from "next/navigation"; + +function LoginForm() { + const router = useRouter(); + + const onSubmit = async (values: { email: string; password: string }) => { + try { + const { data, error } = await authClient.signIn.email({ + email: values.email, + password: values.password, + }); + + if (error) { + throw error; + } + + // Handle 2FA redirect if enabled + if ((data as any).twoFactorRedirect) { + router.replace("/auth/verify"); + return; + } + + // Redirect to dashboard + router.replace("/app/dashboard"); + } catch (e) { + // Handle error + console.error("Login failed:", e); + } + }; + + return ( + <form onSubmit={handleSubmit(onSubmit)}> + {/* Form fields */} + </form> + ); +} +``` + +### Magic Link Sign In + +```typescript +const signInWithMagicLink = async (email: string) => { + const { error } = await authClient.signIn.magicLink({ + email, + callbackURL: "/app/dashboard", + }); + + if (error) { + throw error; + } + + // Show success message - user will receive email + showNotification("Check your email for the login link"); +}; +``` + +### OAuth Sign In + +```typescript +"use client"; +import { authClient } from "@your-app/auth/client"; // Replace with your monorepo package path + +function SocialSigninButton({ provider }: { provider: string }) { + const redirectPath = "/app/dashboard"; + + const onSignin = () => { + const callbackURL = new URL(redirectPath, window.location.origin); + authClient.signIn.social({ + provider, // "google", "github", etc. + callbackURL: callbackURL.toString(), + }); + }; + + return ( + <button onClick={onSignin}> + Sign in with {provider} + </button> + ); +} +``` + +### Passkey Sign In + +```typescript +const signInWithPasskey = async () => { + try { + await authClient.signIn.passkey(); + router.replace("/app/dashboard"); + } catch (e) { + console.error("Passkey authentication failed:", e); + } +}; +``` + +### Sign Out + +```typescript +import { authClient } from "@your-app/auth/client"; // Replace with your monorepo package path + +const onLogout = () => { + authClient.signOut({ + fetchOptions: { + onSuccess: async () => { + // Redirect to home or login page + window.location.href = new URL("/", window.location.origin).toString(); + }, + }, + }); +}; +``` + +## 6. User Profile + +### Accessing Current User + +Use the `useSession` hook to access user data: + +```typescript +function UserProfile() { + const { user, loaded } = useSession(); + + if (!loaded || !user) { + return null; + } + + const { name, email, image } = user; + + return ( + <div className="flex items-center gap-2"> + <img src={image} alt={name} className="w-10 h-10 rounded-full" /> + <div> + <p className="font-medium">{name}</p> + <p className="text-sm text-gray-500">{email}</p> + </div> + </div> + ); +} +``` + +### Updating User Profile + +```typescript +"use client"; +import { authClient } from "@your-app/auth/client"; // Replace with your monorepo package path +import { useSession } from "@/hooks/use-session"; + +function ChangeNameForm() { + const { user, reloadSession } = useSession(); + + const onSubmit = async ({ name }: { name: string }) => { + const { error } = await authClient.updateUser({ + name, + }); + + if (error) { + showError("Failed to update name"); + return; + } + + showSuccess("Name updated successfully"); + + // Reload session to reflect changes + await reloadSession(); + }; + + return ( + <form onSubmit={handleSubmit(onSubmit)}> + <input + type="text" + defaultValue={user?.name ?? ""} + {...register("name")} + /> + <button type="submit">Save</button> + </form> + ); +} +``` + +### Updating Other Profile Fields + +```typescript +// Update avatar +const updateAvatar = async (imageUrl: string) => { + const { error } = await authClient.updateUser({ + image: imageUrl, + }); + + if (!error) { + await reloadSession(); + } +}; + +// Update language preference (if custom field) +const updateLanguage = async (language: string) => { + const { error } = await authClient.updateUser({ + language, + }); + + if (!error) { + await reloadSession(); + } +}; +``` + +## 7. Server-Side Session Access + +For server components, access the session directly: + +```typescript +// lib/server.ts +import "server-only"; +import { auth } from "@your-app/auth"; // Replace with your monorepo package path +import { headers } from "next/headers"; +import { cache } from "react"; + +export const getSession = cache(async () => { + const session = await auth.api.getSession({ + headers: await headers(), + query: { + disableCookieCache: true, + }, + }); + + return session; +}); + +export const getActiveOrganization = cache(async (slug: string) => { + try { + const activeOrganization = await auth.api.getFullOrganization({ + query: { + organizationSlug: slug, + }, + headers: await headers(), + }); + + return activeOrganization; + } catch { + return null; + } +}); +``` + +### Usage in Server Components + +```typescript +// app/(app)/dashboard/page.tsx +import { getSession } from "@/lib/server"; +import { redirect } from "next/navigation"; + +export default async function DashboardPage() { + const session = await getSession(); + + if (!session?.user) { + redirect("/auth/login"); + } + + return ( + <div> + <h1>Welcome, {session.user.name}</h1> + </div> + ); +} +``` + +## 8. Best Practices + +### Always Check Session Before Protected Operations + +```typescript +function DeleteAccountButton() { + const { user, loaded } = useSession(); + + const handleDelete = async () => { + if (!loaded || !user) { + showError("Not authenticated"); + return; + } + + // Proceed with deletion + }; + + return ( + <button onClick={handleDelete} disabled={!loaded || !user}> + Delete Account + </button> + ); +} +``` + +### Handle Loading States Properly + +```typescript +function AuthenticatedComponent() { + const { user, loaded } = useSession(); + + // Always handle loading state first + if (!loaded) { + return <Skeleton />; + } + + // Then handle unauthenticated state + if (!user) { + return <LoginPrompt />; + } + + // Finally render authenticated content + return <ProtectedContent user={user} />; +} +``` + +### Proper Redirect After Auth + +```typescript +function LoginForm() { + const searchParams = useSearchParams(); + const redirectTo = searchParams.get("redirectTo"); + + const onLoginSuccess = () => { + // Redirect to original destination or default + const destination = redirectTo ?? "/app/dashboard"; + router.replace(destination); + }; +} +``` + +### Invalidate Session Cache After Auth Changes + +```typescript +import { useQueryClient } from "@tanstack/react-query"; + +function AuthComponent() { + const queryClient = useQueryClient(); + + const onAuthChange = () => { + // Invalidate session cache to trigger refetch + queryClient.invalidateQueries({ + queryKey: sessionQueryKey, + }); + }; +} +``` + +### Error Handling + +```typescript +const handleAuthError = (error: any) => { + // Get error code from better-auth error + const errorCode = error?.code; + + // Map to user-friendly message + const errorMessages: Record<string, string> = { + INVALID_CREDENTIALS: "Invalid email or password", + USER_NOT_FOUND: "No account found with this email", + EMAIL_NOT_VERIFIED: "Please verify your email first", + TOO_MANY_REQUESTS: "Too many attempts. Please try again later", + }; + + const message = errorMessages[errorCode] ?? "An error occurred"; + showError(message); +}; +``` + +### Security Considerations + +1. **Never store sensitive auth data in localStorage** - better-auth uses secure HTTP-only cookies +2. **Always validate sessions server-side** - Middleware protection is essential +3. **Use HTTPS in production** - Required for secure cookies +4. **Implement CSRF protection** - better-auth handles this automatically +5. **Set appropriate session expiry** - Configure in server auth options + +## 9. Common Patterns + +### Conditional Rendering Based on Auth + +```typescript +function Navigation() { + const { user, loaded } = useSession(); + + return ( + <nav> + <Link href="/">Home</Link> + {loaded && ( + <> + {user ? ( + <> + <Link href="/app/dashboard">Dashboard</Link> + <LogoutButton /> + </> + ) : ( + <> + <Link href="/auth/login">Login</Link> + <Link href="/auth/signup">Sign Up</Link> + </> + )} + </> + )} + </nav> + ); +} +``` + +### Auth State Persistence Across Tabs + +```typescript +// Session is automatically synced via cookies +// For real-time sync, listen to storage events +useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === "auth-sync") { + reloadSession(); + } + }; + + window.addEventListener("storage", handleStorageChange); + return () => window.removeEventListener("storage", handleStorageChange); +}, []); +``` + +### Automatic Session Refresh + +```typescript +// Configure in useSessionQuery +export const useSessionQuery = () => { + return useQuery({ + queryKey: sessionQueryKey, + queryFn: fetchSession, + staleTime: 5 * 60 * 1000, // 5 minutes + refetchInterval: 10 * 60 * 1000, // Refetch every 10 minutes + refetchOnWindowFocus: true, + }); +}; +``` diff --git a/.trellis/spec/frontend/components.md b/.trellis/spec/frontend/components.md new file mode 100644 index 0000000..294df68 --- /dev/null +++ b/.trellis/spec/frontend/components.md @@ -0,0 +1,454 @@ +# Component Development Guidelines + +This document covers component development patterns including Server vs Client components, semantic HTML, and UI best practices. + +## Server vs Client Components + +### Default to Server Components + +Next.js App Router defaults to Server Components. Use them for: + +- Data fetching +- Accessing backend resources directly +- Keeping sensitive data on the server +- Reducing client-side JavaScript + +```typescript +// app/(app)/dashboard/page.tsx (Server Component) +import { DashboardStats } from '@/modules/dashboard/components'; + +export default async function DashboardPage() { + // Can fetch data directly + const stats = await fetchDashboardStats(); + + return ( + <main> + <h1>Dashboard</h1> + <DashboardStats data={stats} /> + </main> + ); +} +``` + +### When to Use Client Components + +Add `'use client'` directive only when you need: + +- Event handlers (onClick, onChange, etc.) +- useState, useEffect, or other React hooks +- Browser-only APIs (localStorage, window) +- Class components with lifecycle methods + +```typescript +'use client'; + +import { useState } from 'react'; + +export function Counter() { + const [count, setCount] = useState(0); + + return ( + <button onClick={() => setCount(count + 1)}> + Count: {count} + </button> + ); +} +``` + +### Composition Pattern + +Keep Server Components at the top, push Client Components down: + +```typescript +// Server Component (page.tsx) +import { ProductList } from './ProductList'; +import { FilterSidebar } from './FilterSidebar'; // Client + +export default async function ProductsPage() { + const products = await fetchProducts(); + + return ( + <div className="flex"> + <FilterSidebar /> {/* Client component for interactivity */} + <ProductList products={products} /> {/* Can be server or client */} + </div> + ); +} +``` + +### Passing Server Data to Client Components + +```typescript +// Server Component +export default async function Page() { + const initialData = await fetchData(); + + return <InteractiveWidget initialData={initialData} />; +} + +// Client Component +'use client'; + +export function InteractiveWidget({ initialData }: { initialData: Data }) { + const [data, setData] = useState(initialData); + // Interactive logic... +} +``` + +## Semantic HTML + +### Use Proper Elements + +```typescript +// Bad: div for everything +<div onClick={handleClick}>Click me</div> +<div> + <div>Item 1</div> + <div>Item 2</div> +</div> + +// Good: semantic elements +<button onClick={handleClick}>Click me</button> +<ul> + <li>Item 1</li> + <li>Item 2</li> +</ul> +``` + +### Button vs Div + +Always use `<button>` for clickable actions: + +```typescript +// Bad: Non-semantic, no keyboard support, no accessibility +<div + className="cursor-pointer" + onClick={handleClick} +> + Save +</div> + +// Good: Semantic, keyboard accessible, proper focus +<button + type="button" + onClick={handleClick} + className="..." +> + Save +</button> +``` + +### Form Elements + +```typescript +// Bad: Missing labels, wrong elements +<div> + <span>Email</span> + <input type="text" /> +</div> + +// Good: Proper form structure +<div> + <label htmlFor="email">Email</label> + <input + id="email" + type="email" + aria-describedby="email-error" + /> + {error && <p id="email-error" role="alert">{error}</p>} +</div> +``` + +### Navigation + +```typescript +// Bad +<div onClick={() => router.push('/about')}>About</div> + +// Good +<Link href="/about">About</Link> + +// For programmatic navigation with button appearance +<Link href="/about" className="btn btn-primary"> + About +</Link> +``` + +## Next.js Image Component + +### Always Use next/image + +```typescript +// Bad: Raw img tag +<img src="/hero.jpg" alt="Hero" /> + +// Good: Optimized Image component +import Image from 'next/image'; + +<Image + src="/hero.jpg" + alt="Hero image" + width={1200} + height={600} + priority // For above-the-fold images +/> +``` + +### Responsive Images + +```typescript +// Fill container +<div className="relative h-64 w-full"> + <Image + src="/banner.jpg" + alt="Banner" + fill + className="object-cover" + sizes="(max-width: 768px) 100vw, 50vw" + /> +</div> +``` + +### Remote Images + +Configure domains in `next.config.js`: + +```javascript +// next.config.js +module.exports = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'images.example.com', + }, + ], + }, +}; +``` + +## Command Palette (cmdk) + +### Basic Implementation + +```typescript +'use client'; + +import { Command } from 'cmdk'; +import { useState, useEffect } from 'react'; + +export function CommandPalette() { + const [open, setOpen] = useState(false); + + // Toggle with keyboard shortcut + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; + + document.addEventListener('keydown', down); + return () => document.removeEventListener('keydown', down); + }, []); + + return ( + <Command.Dialog + open={open} + onOpenChange={setOpen} + label="Global Command Menu" + > + <Command.Input placeholder="Type a command or search..." /> + <Command.List> + <Command.Empty>No results found.</Command.Empty> + + <Command.Group heading="Navigation"> + <Command.Item onSelect={() => router.push('/dashboard')}> + Go to Dashboard + </Command.Item> + <Command.Item onSelect={() => router.push('/settings')}> + Go to Settings + </Command.Item> + </Command.Group> + + <Command.Group heading="Actions"> + <Command.Item onSelect={handleNewOrder}> + Create New Order + </Command.Item> + </Command.Group> + </Command.List> + </Command.Dialog> + ); +} +``` + +### With Search Results + +```typescript +export function SearchCommandPalette() { + const [search, setSearch] = useState(''); + const { data: results, isLoading } = useSearch(search); + + return ( + <Command.Dialog open={open} onOpenChange={setOpen}> + <Command.Input + value={search} + onValueChange={setSearch} + placeholder="Search..." + /> + <Command.List> + {isLoading && <Command.Loading>Searching...</Command.Loading>} + + <Command.Empty>No results found.</Command.Empty> + + {results?.map((item) => ( + <Command.Item + key={item.id} + value={item.title} + onSelect={() => handleSelect(item)} + > + {item.title} + </Command.Item> + ))} + </Command.List> + </Command.Dialog> + ); +} +``` + +## Styling with Tailwind + +### Component Styling Pattern + +```typescript +// Use className for styling +export function Card({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return ( + <div + className={cn( + 'rounded-lg border bg-card p-4 shadow-sm', + className + )} + > + {children} + </div> + ); +} +``` + +### Conditional Styles + +```typescript +import { cn } from '@/lib/utils'; + +export function Button({ + variant = 'primary', + size = 'md', + className, + ...props +}: ButtonProps) { + return ( + <button + className={cn( + 'inline-flex items-center justify-center rounded-md font-medium', + // Variants + { + 'bg-primary text-primary-foreground': variant === 'primary', + 'bg-secondary text-secondary-foreground': variant === 'secondary', + 'border bg-transparent': variant === 'outline', + }, + // Sizes + { + 'h-8 px-3 text-sm': size === 'sm', + 'h-10 px-4': size === 'md', + 'h-12 px-6 text-lg': size === 'lg', + }, + className + )} + {...props} + /> + ); +} +``` + +### Responsive Design + +```typescript +<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> + {items.map((item) => ( + <Card key={item.id}>{item.content}</Card> + ))} +</div> +``` + +## Accessibility + +### Focus Management + +```typescript +export function Modal({ open, onClose, children }: ModalProps) { + const closeButtonRef = useRef<HTMLButtonElement>(null); + + useEffect(() => { + if (open) { + closeButtonRef.current?.focus(); + } + }, [open]); + + return ( + <Dialog open={open} onOpenChange={onClose}> + <DialogContent> + {children} + <button ref={closeButtonRef} onClick={onClose}> + Close + </button> + </DialogContent> + </Dialog> + ); +} +``` + +### ARIA Labels + +```typescript +<button + aria-label="Close dialog" + aria-expanded={isOpen} + aria-controls="dropdown-menu" +> + <CloseIcon /> +</button> + +<div + id="dropdown-menu" + role="menu" + aria-hidden={!isOpen} +> + {/* Menu items */} +</div> +``` + +## Best Practices + +1. **Server First**: Default to Server Components +2. **Semantic HTML**: Use the right element for the job +3. **Optimize Images**: Always use next/image +4. **Accessibility**: Include ARIA labels and keyboard support +5. **Type Props**: Define TypeScript interfaces for all props +6. **Composition**: Break large components into smaller pieces + +## Anti-Patterns + +- Using `div` for buttons and links +- Using `img` instead of `next/image` +- Adding `'use client'` at the top of every file +- Inline styles instead of Tailwind classes +- Missing accessibility attributes +- Components with too many responsibilities diff --git a/.trellis/spec/frontend/css-layout.md b/.trellis/spec/frontend/css-layout.md new file mode 100644 index 0000000..958326a --- /dev/null +++ b/.trellis/spec/frontend/css-layout.md @@ -0,0 +1,381 @@ +# CSS & Layout Best Practices + +This document covers CSS patterns, layout strategies, and cross-environment compatibility considerations. + +## Flexbox Patterns + +### Use items-stretch on Main Flex Containers + +For full-height layouts where children should fill the available space: + +```typescript +// Good: items-stretch (default) allows children to fill height +<div className="flex h-screen"> + <aside className="w-64 bg-gray-100"> + {/* Sidebar fills full height */} + </aside> + <main className="flex-1"> + {/* Main content fills full height */} + </main> +</div> +``` + +```typescript +// Bad: items-center prevents children from filling container height +<div className="flex h-screen items-center"> + <aside className="w-64 bg-gray-100"> + {/* Sidebar only as tall as its content */} + </aside> + <main className="flex-1"> + {/* Main content only as tall as its content */} + </main> +</div> +``` + +### Nested Flex Containers + +```typescript +<div className="flex h-screen flex-col"> + {/* Header - fixed height */} + <header className="h-16 shrink-0 border-b"> + <nav>...</nav> + </header> + + {/* Main area - fills remaining space */} + <div className="flex min-h-0 flex-1"> + {/* Sidebar - fixed width, full height */} + <aside className="w-64 shrink-0 overflow-y-auto border-r"> + <nav>...</nav> + </aside> + + {/* Content - fills remaining width */} + <main className="flex-1 overflow-y-auto"> + <div className="p-6">...</div> + </main> + </div> +</div> +``` + +### min-h-0 for Overflow Control + +When using flex containers with scrollable children: + +```typescript +// Without min-h-0, content may overflow +<div className="flex h-screen flex-col"> + <div className="flex-1"> + {/* This might overflow if content is tall */} + </div> +</div> + +// With min-h-0, overflow is properly contained +<div className="flex h-screen flex-col"> + <div className="min-h-0 flex-1 overflow-y-auto"> + {/* Content scrolls within container */} + </div> +</div> +``` + +## Parent-Child Styling Pattern + +### Parent Provides External Styles + +The parent component controls: +- Positioning (absolute, relative, grid placement) +- External spacing (margin, gap) +- Size constraints (width, max-width) + +```typescript +// Parent component +<div className="grid grid-cols-3 gap-4"> + <Card className="col-span-2" /> {/* Parent sets grid span */} + <Card /> +</div> +``` + +### Child Provides Internal Layout + +The child component controls: +- Internal padding +- Internal layout (flex, grid) +- Background, borders, shadows +- Typography + +```typescript +// Child component +export function Card({ className, children }: CardProps) { + return ( + <div + className={cn( + // Internal styles owned by Card + 'rounded-lg border bg-white p-4 shadow-sm', + // External styles from parent + className + )} + > + {children} + </div> + ); +} +``` + +### Complete Example + +```typescript +// Page layout (parent) +export function DashboardPage() { + return ( + <div className="grid gap-6 p-6 lg:grid-cols-3"> + {/* Parent controls: grid position, external spacing */} + <StatsCard className="lg:col-span-2" /> + <ActivityFeed className="lg:row-span-2" /> + <RecentOrders /> + </div> + ); +} + +// Card component (child) +export function StatsCard({ className }: { className?: string }) { + return ( + <div + className={cn( + // Child controls: internal padding, background, border + 'flex flex-col gap-4 rounded-xl bg-white p-6 shadow', + className + )} + > + {/* Internal layout */} + </div> + ); +} +``` + +## Cross-Environment Testing + +### Dev Mode (Turbopack) vs Production (Webpack) + +CSS may behave differently between development and production builds: + +```bash +# Test in development (Turbopack) +pnpm dev + +# Test in production (Webpack) +pnpm build && pnpm start +``` + +### Common Differences + +1. **CSS Order**: Tailwind classes may be applied in different orders +2. **Purging**: Unused classes removed in production +3. **Minification**: Class names optimized + +### Testing Checklist + +- [ ] Run `pnpm dev` and test all features +- [ ] Run `pnpm build && pnpm start` and test again +- [ ] Check for visual differences +- [ ] Verify responsive breakpoints work +- [ ] Test animations and transitions + +## Mobile Touch Optimization + +### Disable Tap Highlight + +Prevent the default blue/gray highlight on mobile tap: + +```typescript +// Using Tailwind +<button className="[-webkit-tap-highlight-color:transparent]"> + Tap me +</button> + +// Using inline styles (when needed) +<button style={{ WebkitTapHighlightColor: 'transparent' }}> + Tap me +</button> + +// Global reset in CSS +@layer base { + button, a, [role="button"] { + -webkit-tap-highlight-color: transparent; + } +} +``` + +### Touch-Friendly Sizing + +```typescript +// Minimum touch target: 44x44px +<button className="min-h-[44px] min-w-[44px] p-3"> + <Icon size={20} /> +</button> + +// For lists +<ul className="divide-y"> + {items.map((item) => ( + <li key={item.id}> + <button className="w-full px-4 py-3 text-left"> + {item.label} + </button> + </li> + ))} +</ul> +``` + +### Prevent Pull-to-Refresh + +When implementing custom scroll behaviors: + +```typescript +<div + className="h-screen overflow-y-auto overscroll-contain" + style={{ touchAction: 'pan-y' }} +> + {/* Scrollable content */} +</div> +``` + +## Responsive Design Patterns + +### Mobile-First Approach + +```typescript +// Start with mobile styles, add breakpoints for larger screens +<div className=" + p-4 // Mobile: small padding + md:p-6 // Tablet: medium padding + lg:p-8 // Desktop: large padding +"> + <h1 className=" + text-xl // Mobile: small heading + md:text-2xl // Tablet: medium heading + lg:text-3xl // Desktop: large heading + "> + Title + </h1> +</div> +``` + +### Container Queries (Tailwind v4) + +```typescript +// Container-based responsive styles +<div className="@container"> + <div className=" + flex flex-col + @md:flex-row // Row layout when container >= md + @lg:gap-6 // Larger gap when container >= lg + "> + {/* Content */} + </div> +</div> +``` + +### Hiding/Showing Elements + +```typescript +// Hide on mobile, show on desktop +<div className="hidden lg:block"> + Desktop only content +</div> + +// Show on mobile, hide on desktop +<div className="lg:hidden"> + Mobile only content +</div> +``` + +## Z-Index Management + +### Establish a Scale + +```css +/* In your CSS or Tailwind config */ +:root { + --z-dropdown: 10; + --z-sticky: 20; + --z-fixed: 30; + --z-modal-backdrop: 40; + --z-modal: 50; + --z-popover: 60; + --z-tooltip: 70; +} +``` + +### Tailwind Config + +```javascript +// tailwind.config.js +module.exports = { + theme: { + extend: { + zIndex: { + dropdown: '10', + sticky: '20', + fixed: '30', + modalBackdrop: '40', + modal: '50', + popover: '60', + tooltip: '70', + }, + }, + }, +}; +``` + +### Usage + +```typescript +<div className="z-modal">Modal content</div> +<div className="z-tooltip">Tooltip</div> +``` + +## Animation Best Practices + +### Use CSS Transitions + +```typescript +<button className=" + transition-colors duration-200 ease-out + hover:bg-primary-dark +"> + Hover me +</button> +``` + +### Respect Motion Preferences + +```typescript +// Disable animations for users who prefer reduced motion +<div className=" + transition-transform duration-300 + motion-reduce:transition-none + hover:scale-105 + motion-reduce:hover:scale-100 +"> + Animated element +</div> +``` + +### Hardware Acceleration + +```typescript +// Use transform for smooth animations +<div className=" + translate-x-0 transition-transform + group-hover:translate-x-2 +"> + Slides on hover +</div> +``` + +## Best Practices Summary + +1. **items-stretch**: Default for main flex containers +2. **Parent External, Child Internal**: Clear separation of concerns +3. **Test Both Modes**: Always verify in dev AND production +4. **Touch Optimization**: Disable tap highlight, ensure touch targets +5. **Mobile First**: Build up from smallest screens +6. **Consistent Z-Index**: Use a defined scale +7. **Respect Accessibility**: Honor motion preferences diff --git a/.trellis/spec/frontend/directory-structure.md b/.trellis/spec/frontend/directory-structure.md new file mode 100644 index 0000000..c6b9c27 --- /dev/null +++ b/.trellis/spec/frontend/directory-structure.md @@ -0,0 +1,189 @@ +# Directory Structure + +This document describes the module organization and folder conventions for the frontend application. + +## Overview + +``` +app/ # Next.js App Router +├── (marketing)/ # Public marketing pages (i18n) +│ └── [locale]/ # Locale-based routing +└── (app)/ # Protected application routes + └── app/ # Main application routes +modules/ # Feature modules +├── [feature]/ # Feature module +│ ├── components/ # UI components +│ ├── hooks/ # Custom hooks +│ ├── context/ # React Context +│ ├── lib/ # Utilities and data transforms +│ └── types/ # Frontend view model types +├── shared/ # Shared components across features +└── ui/ # UI component library +middleware.ts # Authentication & routing middleware +``` + +## Module Structure + +### Feature Module Pattern + +Each feature module should follow this structure: + +``` +modules/dashboard/ +├── components/ +│ ├── DashboardHeader.tsx +│ ├── StatsCard.tsx +│ ├── ActivityFeed.tsx +│ └── index.ts # Barrel export +├── hooks/ +│ ├── useDashboardStats.ts +│ ├── useActivityFeed.ts +│ └── index.ts +├── context/ +│ ├── DashboardContext.tsx +│ └── index.ts +├── lib/ +│ ├── formatters.ts # Data formatting utilities +│ ├── transformers.ts # API response transformers +│ └── constants.ts # Feature-specific constants +├── types/ +│ └── index.ts # View model types +└── index.ts # Public API of the module +``` + +### Component Organization + +``` +components/ +├── [ComponentName].tsx # Main component file +├── [ComponentName].test.tsx # Unit tests (if applicable) +└── index.ts # Barrel export +``` + +### Hooks Organization + +``` +hooks/ +├── useFeatureData.ts # Data fetching hooks +├── useFeatureActions.ts # Mutation hooks +├── useFeatureState.ts # Local state hooks +└── index.ts +``` + +## Shared Modules + +### `modules/shared/` + +Components and utilities shared across multiple features: + +``` +shared/ +├── components/ +│ ├── Layout/ # Layout components +│ ├── Navigation/ # Navigation components +│ ├── DataTable/ # Reusable data tables +│ └── Forms/ # Form components +├── hooks/ +│ ├── useUser.ts # Current user hook +│ ├── useOrganization.ts # Organization context +│ └── usePermissions.ts # Permission checks +└── lib/ + ├── api.ts # API client configuration + └── utils.ts # Shared utilities +``` + +### `modules/ui/` + +Low-level UI components (design system): + +``` +ui/ +├── Button/ +├── Input/ +├── Select/ +├── Dialog/ +├── Toast/ +└── ... +``` + +## Naming Conventions + +### Files + +| Type | Convention | Example | +|------|------------|---------| +| Components | PascalCase | `UserProfile.tsx` | +| Hooks | camelCase with `use` prefix | `useUserProfile.ts` | +| Context | PascalCase with `Context` suffix | `UserContext.tsx` | +| Utilities | camelCase | `formatDate.ts` | +| Constants | camelCase or SCREAMING_SNAKE_CASE | `constants.ts` | +| Types | PascalCase | `types.ts` or `UserTypes.ts` | + +### Exports + +Use barrel exports (`index.ts`) for clean imports: + +```typescript +// modules/dashboard/components/index.ts +export { DashboardHeader } from './DashboardHeader'; +export { StatsCard } from './StatsCard'; +export { ActivityFeed } from './ActivityFeed'; +``` + +```typescript +// Usage +import { DashboardHeader, StatsCard } from '@/modules/dashboard/components'; +``` + +## Route-Module Mapping + +Routes in `app/(app)/` should map to modules in `modules/`: + +``` +app/(app)/ +├── dashboard/ +│ └── page.tsx -> modules/dashboard/ +├── users/ +│ ├── page.tsx -> modules/users/ +│ └── [id]/ +│ └── page.tsx -> modules/users/ (detail view) +├── settings/ +│ └── page.tsx -> modules/settings/ +└── orders/ + ├── page.tsx -> modules/orders/ + └── [id]/ + └── page.tsx -> modules/orders/ (detail view) +``` + +## Import Path Aliases + +Configure in `tsconfig.json`: + +```json +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"], + "@/modules/*": ["./modules/*"], + "@/components/*": ["./components/*"], + "@/lib/*": ["./lib/*"] + } + } +} +``` + +## Best Practices + +1. **Colocation**: Keep related files close together +2. **Single Responsibility**: Each module should have one clear purpose +3. **Explicit Dependencies**: Import what you need, avoid implicit globals +4. **Barrel Exports**: Use `index.ts` for public APIs +5. **Private by Default**: Only export what needs to be shared + +## Anti-Patterns to Avoid + +- Deeply nested folder structures (max 3-4 levels) +- Circular dependencies between modules +- Mixing feature code with shared utilities +- Importing internal module files directly (use barrel exports) +- Creating "utils" folders that become dumping grounds diff --git a/.trellis/spec/frontend/hooks.md b/.trellis/spec/frontend/hooks.md new file mode 100644 index 0000000..66dbb40 --- /dev/null +++ b/.trellis/spec/frontend/hooks.md @@ -0,0 +1,328 @@ +# Hook Development Patterns + +This document covers React hook patterns for data fetching, mutations, and state management using React Query with oRPC. + +## Query Hooks + +### Basic Query Pattern + +```typescript +import { useQuery } from '@tanstack/react-query'; +import { orpcClient } from '@/lib/orpc'; + +export function useUsers() { + return useQuery({ + queryKey: ['users'], + queryFn: () => orpcClient.users.list(), + }); +} +``` + +### Query with Parameters + +```typescript +export function useUser(userId: string) { + return useQuery({ + queryKey: ['users', userId], + queryFn: () => orpcClient.users.get({ id: userId }), + enabled: !!userId, // Only fetch when userId is available + }); +} +``` + +### Query with Filters + +```typescript +interface UseOrdersOptions { + status?: string; + page?: number; + pageSize?: number; +} + +export function useOrders(options: UseOrdersOptions = {}) { + const { status, page = 1, pageSize = 20 } = options; + + return useQuery({ + queryKey: ['orders', { status, page, pageSize }], + queryFn: () => orpcClient.orders.list({ status, page, pageSize }), + placeholderData: (previousData) => previousData, // Keep previous data while fetching + }); +} +``` + +## Mutation Hooks + +### Basic Mutation Pattern + +```typescript +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { orpcClient } from '@/lib/orpc'; + +export function useCreateUser() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: CreateUserInput) => orpcClient.users.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }); + }, + }); +} +``` + +### Mutation with Optimistic Updates + +```typescript +type OrderListData = Awaited<ReturnType<typeof orpcClient.orders.list>>; + +export function useUpdateOrderStatus() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, status }: { id: string; status: string }) => + orpcClient.orders.updateStatus({ id, status }), + + onMutate: async ({ id, status }) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: ['orders'] }); + + // Snapshot previous value + const previousOrders = queryClient.getQueryData<OrderListData>(['orders']); + + // Optimistically update + queryClient.setQueryData<OrderListData>(['orders'], (old) => { + if (!old) return old; + return { + ...old, + items: old.items.map((order) => + order.id === id ? { ...order, status } : order + ), + }; + }); + + return { previousOrders }; + }, + + onError: (_err, _variables, context) => { + // Rollback on error + if (context?.previousOrders) { + queryClient.setQueryData(['orders'], context.previousOrders); + } + }, + + onSettled: () => { + // Always refetch after mutation + queryClient.invalidateQueries({ queryKey: ['orders'] }); + }, + }); +} +``` + +## Overriding Mutation Callbacks + +When overriding mutation callbacks at the call site, you MUST add explicit generics to maintain type safety: + +### Problem: Lost Type Safety + +```typescript +// Hook definition +export function useDeleteUser() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => orpcClient.users.delete({ id }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }); + }, + }); +} + +// Bad: Overriding without generics loses type safety +const deleteUser = useDeleteUser(); +deleteUser.mutate(userId, { + onSuccess: (data) => { + // 'data' is typed as 'unknown' here! + console.log(data.id); // TypeScript error or runtime error + }, +}); +``` + +### Solution: Explicit Generics + +```typescript +// Infer types for the mutation +type DeleteUserData = Awaited<ReturnType<typeof orpcClient.users.delete>>; +type DeleteUserVariables = string; + +// Good: Add explicit generics when overriding callbacks +deleteUser.mutate<DeleteUserData, Error, DeleteUserVariables>(userId, { + onSuccess: (data) => { + // 'data' is properly typed + console.log(data.id); // Works correctly + }, +}); +``` + +### Alternative: Define Types in Hook + +```typescript +// Export types from the hook file +export type DeleteUserMutationData = Awaited< + ReturnType<typeof orpcClient.users.delete> +>; + +// Usage with exported types +deleteUser.mutate(userId, { + onSuccess: (data: DeleteUserMutationData) => { + console.log(data.id); + }, +}); +``` + +## Using orpcClient Directly in Hooks + +Inside hooks, use `orpcClient` directly instead of wrapping with `useMutation`: + +### DO: Direct orpcClient Usage + +```typescript +export function useOrderActions() { + const queryClient = useQueryClient(); + + const updateOrder = useMutation({ + mutationFn: (data: UpdateOrderInput) => orpcClient.orders.update(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['orders'] }); + }, + }); + + const deleteOrder = useMutation({ + mutationFn: (id: string) => orpcClient.orders.delete({ id }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['orders'] }); + }, + }); + + return { + updateOrder: updateOrder.mutate, + deleteOrder: deleteOrder.mutate, + isUpdating: updateOrder.isPending, + isDeleting: deleteOrder.isPending, + }; +} +``` + +### DON'T: Nested Hooks + +```typescript +// Bad: Don't create hooks that use other mutation hooks +export function useOrderActions() { + // Don't do this - creates unnecessary abstraction + const updateMutation = useUpdateOrder(); + const deleteMutation = useDeleteOrder(); + + return { + updateOrder: updateMutation.mutate, + deleteOrder: deleteMutation.mutate, + }; +} +``` + +## Compound Hooks + +Combine related queries and mutations into a single hook: + +```typescript +export function useProduct(productId: string) { + const queryClient = useQueryClient(); + + const query = useQuery({ + queryKey: ['products', productId], + queryFn: () => orpcClient.products.get({ id: productId }), + enabled: !!productId, + }); + + const update = useMutation({ + mutationFn: (data: UpdateProductInput) => + orpcClient.products.update({ id: productId, ...data }), + onSuccess: (updatedProduct) => { + queryClient.setQueryData(['products', productId], updatedProduct); + }, + }); + + const remove = useMutation({ + mutationFn: () => orpcClient.products.delete({ id: productId }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['products'] }); + }, + }); + + return { + product: query.data, + isLoading: query.isLoading, + error: query.error, + updateProduct: update.mutate, + deleteProduct: remove.mutate, + isUpdating: update.isPending, + isDeleting: remove.isPending, + }; +} +``` + +## Infinite Query Pattern + +```typescript +export function useInfiniteOrders() { + return useInfiniteQuery({ + queryKey: ['orders', 'infinite'], + queryFn: ({ pageParam = 1 }) => + orpcClient.orders.list({ page: pageParam, pageSize: 20 }), + getNextPageParam: (lastPage) => + lastPage.hasMore ? lastPage.page + 1 : undefined, + initialPageParam: 1, + }); +} +``` + +## Dependent Queries + +```typescript +export function useUserOrders(userId: string) { + // First query: get user + const userQuery = useQuery({ + queryKey: ['users', userId], + queryFn: () => orpcClient.users.get({ id: userId }), + enabled: !!userId, + }); + + // Second query: depends on user data + const ordersQuery = useQuery({ + queryKey: ['orders', { userId }], + queryFn: () => orpcClient.orders.list({ userId }), + enabled: !!userQuery.data, // Only run when user is loaded + }); + + return { + user: userQuery.data, + orders: ordersQuery.data, + isLoading: userQuery.isLoading || ordersQuery.isLoading, + }; +} +``` + +## Best Practices + +1. **Single Responsibility**: Each hook should have one clear purpose +2. **Consistent Naming**: `useXxx` for hooks, `useXxxQuery` for queries, `useXxxMutation` for mutations +3. **Error Handling**: Always consider error states in your hooks +4. **Loading States**: Expose loading states for UI feedback +5. **Cache Keys**: Use consistent, hierarchical query keys +6. **Type Safety**: Always maintain proper TypeScript types + +## Common Pitfalls + +- Forgetting to invalidate related queries after mutations +- Not handling race conditions with `cancelQueries` +- Missing `enabled` flag for conditional queries +- Not providing explicit generics when overriding callbacks +- Creating too many small hooks instead of compound hooks diff --git a/.trellis/spec/frontend/index.md b/.trellis/spec/frontend/index.md new file mode 100644 index 0000000..b40cbd8 --- /dev/null +++ b/.trellis/spec/frontend/index.md @@ -0,0 +1,125 @@ +# Next.js Frontend Development Guidelines + +> Universal frontend development guidelines for Next.js full-stack applications with React + TypeScript + TailwindCSS. + +## Tech Stack + +- **Framework**: Next.js 15, React 19 +- **Language**: TypeScript (strict mode) +- **Styling**: TailwindCSS 4, Radix UI +- **API**: oRPC (OpenAPI RPC), React Query (TanStack Query) +- **URL State**: nuqs +- **Auth**: better-auth +- **AI**: Vercel AI SDK (@ai-sdk/react) + +--- + +## Documentation Files + +| File | Description | Priority | +| ---------------------------------------------------- | ---------------------------------------------------- | ------------- | +| [components.md](./components.md) | Server/Client components, semantic HTML, next/image | **Must Read** | +| [authentication.md](./authentication.md) | better-auth client, session, protected routes | **Must Read** | +| [orpc-usage.md](./orpc-usage.md) | Type-safe API calls, React Query integration | **Must Read** | +| [hooks.md](./hooks.md) | Query and mutation hook patterns | Reference | +| [api-integration.md](./api-integration.md) | oRPC client, real-time, AI streaming | Reference | +| [state-management.md](./state-management.md) | URL state with nuqs, React Context patterns | Reference | +| [directory-structure.md](./directory-structure.md) | Project structure and module conventions | Reference | +| [type-safety.md](./type-safety.md) | TypeScript guidelines, type inference, Zod | Reference | +| [css-layout.md](./css-layout.md) | CSS patterns, flexbox, responsive, touch | Reference | +| [ai-sdk-integration.md](./ai-sdk-integration.md) | useChat hook, streaming, tool call handling | Reference | +| [quality.md](./quality.md) | Pre-commit checklist and code quality standards | Reference | + +--- + +## Quick Navigation by Task + +### Before Starting Development + +| Task | Document | +| --------------------------------- | -------------------------------------------------- | +| Understand project structure | [directory-structure.md](./directory-structure.md) | +| Learn Server vs Client components | [components.md](./components.md) | +| Set up authentication | [authentication.md](./authentication.md) | + +### During Development + +| Task | Document | +| --------------------------- | -------------------------------------------------- | +| Make type-safe API calls | [orpc-usage.md](./orpc-usage.md) | +| Create custom hooks | [hooks.md](./hooks.md) | +| Manage application state | [state-management.md](./state-management.md) | +| Build UI components | [components.md](./components.md) | +| Ensure type safety | [type-safety.md](./type-safety.md) | +| Integrate AI features | [ai-sdk-integration.md](./ai-sdk-integration.md) | +| Handle CSS & layout | [css-layout.md](./css-layout.md) | + +### Before Committing + +| Task | Document | +| ----------------------- | -------------------------------- | +| Run quality checklist | [quality.md](./quality.md) | +| Verify CSS in both envs | [css-layout.md](./css-layout.md) | +| Check type safety | [type-safety.md](./type-safety.md) | + +--- + +## Core Rules Summary + +| Rule | Reference | +| ------------------------------------------------------------ | -------------------------------------------------- | +| **Default to Server Components** | [components.md](./components.md) | +| **Use `<button>` for clickable actions, not `<div>`** | [components.md](./components.md) | +| **Always use `next/image` instead of `<img>`** | [components.md](./components.md) | +| **Import types from backend, never redefine them** | [type-safety.md](./type-safety.md) | +| **No `any` types or `@ts-expect-error` in new code** | [type-safety.md](./type-safety.md) | +| **Use oRPC client for API calls (not raw fetch)** | [orpc-usage.md](./orpc-usage.md) | +| **Use oRPC generated query keys (not manual strings)** | [orpc-usage.md](./orpc-usage.md) | +| **Store shareable state in URL with nuqs** | [state-management.md](./state-management.md) | +| **Use `items-stretch` on main flex containers** | [css-layout.md](./css-layout.md) | +| **Handle both tool call formats (streaming + history)** | [ai-sdk-integration.md](./ai-sdk-integration.md) | +| **Always check session loading state before rendering** | [authentication.md](./authentication.md) | + +--- + +## Architecture Overview + +``` ++--------------------------------------------------------------+ +| Next.js Application | +| | +| app/ modules/ | +| ├── (marketing)/ ├── [feature]/ | +| │ └── [locale]/ │ ├── components/ | +| └── (app)/ │ ├── hooks/ | +| └── [routes]/ │ ├── context/ | +| │ └── lib/ | +| ├── shared/ | +| └── ui/ | ++-------------------------------+------------------------------+ + | + oRPC (type-safe RPC) | React Query (cache) + | ++-------------------------------+------------------------------+ +| API Layer (Server) | +| +--------------+ +----------------+ +------------------+ | +| | oRPC | | better-auth | | AI SDK | | +| | Router | | Sessions | | Streaming | | +| +--------------+ +----------------+ +------------------+ | ++--------------------------------------------------------------+ +``` + +--- + +## Getting Started + +1. **Read the Must-Read documents** - Components, authentication, and oRPC usage +2. **Set up your project structure** - Follow [directory-structure.md](./directory-structure.md) +3. **Configure TypeScript paths** - See [type-safety.md](./type-safety.md) +4. **Set up API client** - Use patterns from [orpc-usage.md](./orpc-usage.md) +5. **Build components** - Follow [components.md](./components.md) and [hooks.md](./hooks.md) +6. **Before committing** - Complete the [quality.md](./quality.md) checklist + +--- + +**Language**: All documentation is written in **English**. diff --git a/.trellis/spec/frontend/orpc-usage.md b/.trellis/spec/frontend/orpc-usage.md new file mode 100644 index 0000000..8dad0f8 --- /dev/null +++ b/.trellis/spec/frontend/orpc-usage.md @@ -0,0 +1,662 @@ +# oRPC Frontend Usage Guidelines + +This document provides comprehensive guidelines for using oRPC in frontend applications, covering client setup, React Query integration, and best practices. + +## 1. Overview + +oRPC (OpenAPI RPC) provides type-safe RPC-style API calls with automatic TypeScript type inference. When combined with React Query (TanStack Query), it offers a powerful solution for data fetching, caching, and state synchronization. + +**Key Benefits:** +- End-to-end type safety from backend to frontend +- Automatic query key generation +- Seamless React Query integration +- Built-in error handling + +## 2. Client Setup + +### Basic Client Configuration + +```typescript +// lib/orpc-client.ts +import { createORPCClient, onError } from "@orpc/client"; +import { RPCLink } from "@orpc/client/fetch"; +import type { ApiRouterClient } from "@your-app/api/orpc/router"; // Replace with your monorepo package path + +const link = new RPCLink({ + url: "/api/rpc", + headers: async () => { + // Client-side: return empty headers (cookies handled automatically) + if (typeof window !== "undefined") { + return {}; + } + // Server-side: forward request headers for SSR + const { headers } = await import("next/headers"); + return Object.fromEntries(await headers()); + }, + interceptors: [ + onError((error) => { + // Ignore abort errors (e.g., from React strict mode) + if (error instanceof Error && error.name === "AbortError") { + return; + } + console.error(error); + }), + ], +}); + +export const orpcClient: ApiRouterClient = createORPCClient(link); +``` + +**Key Points:** +- The `ApiRouterClient` type ensures full type safety +- Headers handling differs between client and server environments +- Error interceptors provide centralized error logging + +## 3. React Query Integration + +### Creating Query Utilities + +```typescript +// lib/orpc-query-utils.ts +import { createTanstackQueryUtils } from "@orpc/tanstack-query"; +import { orpcClient } from "./orpc-client"; + +export const orpc = createTanstackQueryUtils(orpcClient); +``` + +The `orpc` object provides utilities for generating query options and keys that integrate seamlessly with React Query. + +## 4. Query Patterns + +### 4.1 Basic Query with useQuery + +```typescript +import { orpc } from "@/lib/orpc-query-utils"; +import { orpcClient } from "@/lib/orpc-client"; +import { useQuery } from "@tanstack/react-query"; + +// Derive types from the client +type ItemResult = Awaited<ReturnType<(typeof orpcClient)["items"]["get"]>>; + +export function useItem(itemId: string | null) { + const hasItemId = typeof itemId === "string" && itemId.trim().length > 0; + + const { data, isLoading, error, refetch } = useQuery<ItemResult | undefined>({ + ...orpc.items.get.queryOptions({ + input: { itemId: itemId ?? "" }, + }), + enabled: hasItemId, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + gcTime: 10 * 60 * 1000, // Keep in garbage collection for 10 minutes + }); + + return { + item: data?.item ?? null, + isLoading, + error, + refetch, + }; +} +``` + +### 4.2 Infinite Query with Cursor Pagination + +```typescript +import { orpc } from "@/lib/orpc-query-utils"; +import { orpcClient } from "@/lib/orpc-client"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import type { InfiniteData } from "@tanstack/react-query"; + +type ListResult = Awaited<ReturnType<(typeof orpcClient)["items"]["list"]>>; +type ListCursor = { lastUpdatedAt: string; id: string } | undefined; +type ListQueryKey = ReturnType<typeof orpc.items.list.queryKey>; + +interface UseItemListOptions { + categoryId: string | null; + filters?: { + isActive?: boolean; + search?: string; + }; + enabled?: boolean; +} + +export function useItemList(options: UseItemListOptions) { + const { categoryId, filters, enabled = true } = options; + + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + error, + refetch, + } = useInfiniteQuery< + ListResult, + Error, + InfiniteData<ListResult, ListCursor>, + ListQueryKey, + ListCursor + >({ + queryKey: orpc.items.list.queryKey({ + input: { + categoryId: categoryId ?? "", + filters, + }, + }), + queryFn: async ({ pageParam }): Promise<ListResult> => { + if (!categoryId) { + throw new Error("Category ID is required"); + } + return await orpcClient.items.list({ + categoryId, + limit: 20, + cursor: pageParam, + filters, + }); + }, + initialPageParam: undefined, + getNextPageParam: (lastPage): ListCursor => + lastPage.nextCursor ?? undefined, + enabled: enabled && !!categoryId, + }); + + // Flatten all pages into a single array + const items = data?.pages.flatMap((page) => page.items) ?? []; + + return { + items, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + isLoading, + error, + refetch, + }; +} +``` + +### 4.3 Batch Queries with useQueries + +```typescript +import { orpc } from "@/lib/orpc-query-utils"; +import { useQueries } from "@tanstack/react-query"; +import { useMemo } from "react"; + +interface UseBatchItemsOptions { + itemIds: string[]; + enabled?: boolean; + staleTime?: number; +} + +export function useBatchItems(options: UseBatchItemsOptions) { + const { itemIds, enabled = true, staleTime = 5 * 60 * 1000 } = options; + + const queries = useQueries({ + queries: itemIds.map((itemId) => ({ + ...orpc.items.get.queryOptions({ + input: { itemId }, + }), + enabled: enabled && !!itemId, + staleTime, + })), + }); + + // Build a map for easy lookup + const itemsMap = useMemo(() => { + const map = new Map(); + queries.forEach((query, index) => { + const itemId = itemIds[index]; + if (itemId && query.data) { + map.set(itemId, { + data: query.data, + isLoading: query.isLoading, + error: query.error, + }); + } + }); + return map; + }, [queries, itemIds]); + + return { + itemsMap, + isLoading: queries.some((q) => q.isLoading), + isAllLoaded: queries.every((q) => q.isSuccess || q.isError), + }; +} +``` + +### 4.4 Query Key Management + +oRPC provides automatic query key generation: + +```typescript +// Get query key with input parameters +const queryKey = orpc.items.list.queryKey({ + input: { categoryId: "123", filters: { isActive: true } }, +}); + +// Get base key (without input) for broader invalidation +const baseKey = orpc.items.list.key(); + +// Usage in cache invalidation +queryClient.invalidateQueries({ + queryKey: orpc.items.list.key(), // Invalidates all items.list queries +}); + +queryClient.invalidateQueries({ + queryKey: orpc.items.list.queryKey({ + input: { categoryId: "123" }, + }), // Invalidates specific query +}); +``` + +## 5. Mutation Patterns + +### 5.1 Basic Mutation + +```typescript +import { orpc } from "@/lib/orpc-query-utils"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; + +interface UseConnectServiceOptions { + onSuccess?: (data: ConnectOutput) => void; + onError?: (error: Error) => void; +} + +export function useConnectService(options: UseConnectServiceOptions = {}) { + const queryClient = useQueryClient(); + + return useMutation<ConnectOutput, Error, ConnectInput>({ + ...orpc.services.connect.mutationOptions(), + onSuccess: (data) => { + // Invalidate related queries + queryClient.invalidateQueries({ + queryKey: orpc.services.default.key(), + }); + options.onSuccess?.(data); + }, + onError: (error) => { + toast.error("Connection failed", { + description: error.message, + }); + options.onError?.(error); + }, + }); +} +``` + +### 5.2 Mutation with Optimistic Updates + +```typescript +import { orpc } from "@/lib/orpc-query-utils"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; + +export function useUpdateItem() { + const queryClient = useQueryClient(); + + return useMutation< + { success: boolean }, + Error, + { itemId: string; isActive: boolean }, + { previousQueries: [readonly unknown[], unknown][] } + >({ + ...orpc.items.update.mutationOptions(), + onMutate: async ({ itemId, isActive }) => { + // Cancel outgoing refetches to avoid overwriting optimistic update + await queryClient.cancelQueries({ + queryKey: orpc.items.list.key(), + }); + + // Snapshot current data for rollback + const previousQueries = queryClient.getQueriesData({ + queryKey: orpc.items.list.key(), + }); + + // Optimistically update the cache + queryClient.setQueriesData( + { queryKey: orpc.items.list.key() }, + (old: unknown) => { + const data = old as { + pages?: Array<{ + items: Array<{ id: string; isActive: boolean }>; + }>; + }; + if (!data?.pages) return old; + + return { + ...data, + pages: data.pages.map((page) => ({ + ...page, + items: page.items.map((item) => + item.id === itemId ? { ...item, isActive } : item + ), + })), + }; + } + ); + + return { previousQueries }; + }, + onError: (_error, _variables, context) => { + // Rollback on error + if (context?.previousQueries) { + for (const [queryKey, data] of context.previousQueries) { + queryClient.setQueryData(queryKey, data); + } + } + toast.error("Failed to update item"); + }, + onSuccess: () => { + // Optionally invalidate related queries + queryClient.invalidateQueries({ + queryKey: orpc.items.counts.key(), + }); + }, + }); +} +``` + +### 5.3 Optimistic Delete (Remove from List) + +```typescript +export function useDeleteItem() { + const queryClient = useQueryClient(); + + return useMutation< + { success: boolean }, + Error, + { itemId: string }, + { previousQueries: [readonly unknown[], unknown][] } + >({ + ...orpc.items.delete.mutationOptions(), + onMutate: async ({ itemId }) => { + await queryClient.cancelQueries({ + queryKey: orpc.items.list.key(), + }); + + const previousQueries = queryClient.getQueriesData({ + queryKey: orpc.items.list.key(), + }); + + // Optimistically remove from all lists + queryClient.setQueriesData( + { queryKey: orpc.items.list.key() }, + (old: unknown) => { + const data = old as { + pages?: Array<{ + items: Array<{ id: string }>; + nextCursor: unknown; + hasMore: boolean; + }>; + }; + if (!data?.pages) return old; + + return { + ...data, + pages: data.pages.map((page) => ({ + ...page, + items: page.items.filter((item) => item.id !== itemId), + })), + }; + } + ); + + return { previousQueries }; + }, + onError: (_error, _variables, context) => { + if (context?.previousQueries) { + for (const [queryKey, data] of context.previousQueries) { + queryClient.setQueryData(queryKey, data); + } + } + toast.error("Failed to delete item"); + }, + onSuccess: () => { + toast.success("Item deleted"); + queryClient.invalidateQueries({ + queryKey: orpc.items.counts.key(), + }); + }, + }); +} +``` + +## 6. Direct Client Calls + +### 6.1 When to Use Direct Client vs useMutation + +**Use `useMutation` when:** +- You need loading/error states in UI +- You want automatic retry behavior +- You need optimistic updates +- You want built-in cache invalidation hooks + +**Use direct `orpcClient` calls when:** +- Inside `mutationFn` for custom logic (see 6.2) +- In event handlers where you need sequential operations +- When you need to transform input before calling API +- In server components or API routes + +### 6.2 Custom Mutation Function + +When you need to add custom logic, transform inputs, or handle complex scenarios: + +```typescript +import { orpcClient } from "@/lib/orpc-client"; +import { orpc } from "@/lib/orpc-query-utils"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +export function useCreateItem(options = {}) { + const { user } = useSession(); + const queryClient = useQueryClient(); + + return useMutation< + CreateItemOutput, + Error, + Omit<CreateItemInput, "userId"> // userId will be added automatically + >({ + mutationKey: orpc.items.create.mutationKey(), + mutationFn: async (input) => { + // Add authentication + if (!user?.id) { + throw new Error("User not authenticated"); + } + + // Transform input before calling API + const fullInput: CreateItemInput = { + ...input, + userId: user.id, + }; + + // Direct client call with transformed input + return orpcClient.items.create(fullInput); + }, + onSuccess: (data) => { + if (data.success) { + toast.success("Item created successfully"); + queryClient.invalidateQueries({ + queryKey: orpc.items.list.key(), + }); + options.onSuccess?.(data); + } else { + // Handle API-level errors + const errorMessage = data.error || "Failed to create item"; + toast.error("Failed to create item", { + description: errorMessage, + }); + options.onError?.(new Error(errorMessage)); + } + }, + onError: (error) => { + toast.error("Failed to create item", { + description: error.message, + }); + options.onError?.(error); + }, + }); +} +``` + +### 6.3 Type Inference + +Derive types directly from the client for maximum type safety: + +```typescript +import type { orpcClient } from "@/lib/orpc-client"; + +// Infer return type from client method +type ItemResult = Awaited<ReturnType<(typeof orpcClient)["items"]["get"]>>; + +// Infer input type from client method +type CreateItemInput = Parameters<(typeof orpcClient)["items"]["create"]>[0]; +``` + +## 7. Best Practices + +### 7.1 Query Key Consistency + +Always use oRPC's generated query keys for consistency: + +```typescript +// GOOD: Use generated query keys +queryClient.invalidateQueries({ + queryKey: orpc.items.list.key(), +}); + +// GOOD: Use specific query key with input +queryClient.invalidateQueries({ + queryKey: orpc.items.list.queryKey({ input: { categoryId: "123" } }), +}); + +// BAD: Manually constructed keys +queryClient.invalidateQueries({ + queryKey: ["items", "list"], // Don't do this +}); +``` + +### 7.2 Error Handling + +Implement consistent error handling with toast notifications: + +```typescript +import { toast } from "sonner"; + +// In mutation hooks +onError: (error) => { + toast.error("Operation failed", { + description: error.message, + }); +}, + +// Handle API-level errors in onSuccess +onSuccess: (data) => { + if (!data.success) { + toast.error("Operation failed", { + description: data.error || "Unknown error", + }); + return; + } + // Handle success... +}, +``` + +### 7.3 Loading States + +Use appropriate loading state properties: + +```typescript +const { isLoading, isFetching, isPending } = useQuery(...); +const { isPending, isSuccess, isError } = useMutation(...); +const { isFetchingNextPage, hasNextPage } = useInfiniteQuery(...); + +// In components +{isLoading && <Skeleton />} +{isPending && <Button disabled>Saving...</Button>} +{isFetchingNextPage && <LoadingSpinner />} +``` + +### 7.4 Cache Configuration + +Set appropriate cache times based on data characteristics: + +```typescript +// Frequently changing data +staleTime: 30 * 1000, // 30 seconds +gcTime: 60 * 1000, // 1 minute + +// Moderately stable data +staleTime: 5 * 60 * 1000, // 5 minutes +gcTime: 10 * 60 * 1000, // 10 minutes + +// Stable/static data +staleTime: 30 * 60 * 1000, // 30 minutes +gcTime: 60 * 60 * 1000, // 1 hour +``` + +### 7.5 Input Validation in Hooks + +Always validate inputs before making API calls: + +```typescript +export function useItem(itemId: string | null) { + const hasItemId = typeof itemId === "string" && itemId.trim().length > 0; + + useEffect(() => { + if (!hasItemId) { + console.warn("[useItem] Invalid itemId provided. Request skipped."); + } + }, [hasItemId]); + + return useQuery({ + ...orpc.items.get.queryOptions({ + input: { itemId: itemId ?? "" }, + }), + enabled: hasItemId, // Prevent invalid requests + }); +} +``` + +### 7.6 Partial Success Handling + +Handle batch operations that may partially succeed: + +```typescript +onSuccess: (result) => { + if (result.failed === 0) { + toast.success(`${result.processed} items updated`); + } else if (result.processed > 0) { + toast.warning( + `${result.processed} of ${result.total} items updated, ${result.failed} failed` + ); + // Refresh to get correct state for failed items + queryClient.invalidateQueries({ + queryKey: orpc.items.list.key(), + }); + } else { + toast.error("Failed to update items"); + } +}, +``` + +## 8. Common Patterns Summary + +| Pattern | Hook | Use Case | +|---------|------|----------| +| Single item fetch | `useQuery` | Detail pages, single record | +| List with pagination | `useInfiniteQuery` | Lists, feeds, search results | +| Multiple items | `useQueries` | Batch preloading, related items | +| Create/Update/Delete | `useMutation` | Form submissions, actions | +| Optimistic updates | `useMutation` + `onMutate` | Real-time UI updates | +| Custom mutation logic | `useMutation` + `mutationFn` | Auth injection, input transformation | + +## 9. Migration Notes + +When migrating from other data fetching approaches: + +1. Replace manual fetch calls with `orpcClient` methods +2. Replace manual query keys with `orpc.xxx.queryKey()` +3. Use `orpc.xxx.queryOptions()` and `mutationOptions()` for React Query integration +4. Leverage TypeScript inference from the client types diff --git a/.trellis/spec/frontend/quality.md b/.trellis/spec/frontend/quality.md new file mode 100644 index 0000000..6772efb --- /dev/null +++ b/.trellis/spec/frontend/quality.md @@ -0,0 +1,137 @@ +# Pre-commit Checklist + +Complete this checklist before committing frontend code changes. + +## Type Safety + +- [ ] No `@ts-expect-error` or `@ts-ignore` comments added +- [ ] No `any` types in new code +- [ ] API response types are inferred or imported from backend (not redefined) +- [ ] Cache updates in React Query are properly typed +- [ ] When overriding mutation callbacks, explicit generics are provided + +## Component Development + +- [ ] Server Components used by default; `'use client'` only when necessary +- [ ] Semantic HTML elements used (button, not div for clicks) +- [ ] `next/image` used instead of `<img>` tags +- [ ] Proper ARIA labels and accessibility attributes added +- [ ] Props have TypeScript interfaces defined + +## API Integration + +- [ ] API calls use oRPC client (not raw fetch for internal APIs) +- [ ] React Query hooks follow established patterns +- [ ] Loading and error states handled +- [ ] Optimistic updates include rollback logic +- [ ] Real-time subscriptions cleaned up on unmount + +## State Management + +- [ ] Shareable state stored in URL with nuqs +- [ ] Context used sparingly (not for server data) +- [ ] URL and Context synchronized where necessary +- [ ] No duplicate state across different systems + +## CSS & Layout + +- [ ] `items-stretch` used on main flex containers (not `items-center`) +- [ ] Parent provides external styles; child provides internal layout +- [ ] Mobile touch: `WebkitTapHighlightColor: "transparent"` applied +- [ ] Touch targets are minimum 44x44px +- [ ] Responsive breakpoints tested + +## Cross-Environment Testing + +- [ ] Tested in development mode (`pnpm dev`) +- [ ] Tested in production mode (`pnpm build && pnpm start`) +- [ ] No visual differences between dev and prod +- [ ] Animations respect `prefers-reduced-motion` + +## Code Quality + +- [ ] No console.log statements left in code +- [ ] Unused imports removed +- [ ] Components follow single responsibility principle +- [ ] File and function names follow conventions +- [ ] Barrel exports updated if new files added + +## Documentation + +- [ ] Complex logic has inline comments +- [ ] New hooks have JSDoc comments +- [ ] API changes reflected in backend documentation + +--- + +## Quick Commands + +```bash +# Type check +pnpm type-check + +# Lint +pnpm lint + +# Format +pnpm format + +# Build (catches production-only issues) +pnpm build + +# Run all checks +pnpm lint && pnpm type-check && pnpm build +``` + +## Common Issues to Watch + +### Type Safety +```typescript +// Bad +queryClient.setQueryData(['users'], (old: any) => ...) + +// Good +queryClient.setQueryData<UserListData>(['users'], (old) => ...) +``` + +### Components +```typescript +// Bad +<div onClick={handleClick}>Click me</div> + +// Good +<button onClick={handleClick}>Click me</button> +``` + +### Images +```typescript +// Bad +<img src="/hero.jpg" alt="Hero" /> + +// Good +import Image from 'next/image'; +<Image src="/hero.jpg" alt="Hero" width={1200} height={600} /> +``` + +### Layout +```typescript +// Bad - children won't fill height +<div className="flex h-screen items-center"> + +// Good - children fill available height +<div className="flex h-screen"> +``` + +### Mobile Touch +```typescript +// Bad - shows tap highlight on mobile +<button onClick={handleClick}>Tap</button> + +// Good - no tap highlight +<button + onClick={handleClick} + style={{ WebkitTapHighlightColor: 'transparent' }} +> + Tap +</button> +``` diff --git a/.trellis/spec/frontend/state-management.md b/.trellis/spec/frontend/state-management.md new file mode 100644 index 0000000..3373189 --- /dev/null +++ b/.trellis/spec/frontend/state-management.md @@ -0,0 +1,372 @@ +# State Management + +This document covers state management patterns including URL state with nuqs, React Context guidelines, and synchronization strategies. + +## State Categories + +| Category | Tool | When to Use | +|----------|------|-------------| +| Server State | React Query | API data, cached responses | +| URL State | nuqs | Filters, pagination, selected items | +| Local UI State | useState | Transient UI (modals, dropdowns) | +| Shared UI State | Context | Cross-component UI state | + +## URL State with nuqs + +### Why URL State? + +- Shareable: Users can share links with specific state +- Bookmarkable: Browser history navigation works +- SEO-friendly: Search engines can index different states +- Persistent: Survives page refreshes + +### Basic Usage + +```typescript +import { useQueryState } from 'nuqs'; + +export function useOrderFilters() { + const [status, setStatus] = useQueryState('status'); + const [page, setPage] = useQueryState('page', { + parse: (value) => parseInt(value, 10) || 1, + serialize: String, + }); + + return { status, setStatus, page, setPage }; +} +``` + +### With Default Values + +```typescript +import { useQueryState, parseAsInteger, parseAsString } from 'nuqs'; + +export function useProductFilters() { + const [category, setCategory] = useQueryState('category', { + defaultValue: 'all', + parse: parseAsString, + }); + + const [page, setPage] = useQueryState('page', { + defaultValue: 1, + parse: parseAsInteger, + }); + + const [sortBy, setSortBy] = useQueryState('sort', { + defaultValue: 'newest', + }); + + return { + category, + setCategory, + page, + setPage, + sortBy, + setSortBy, + }; +} +``` + +### Complex Filter Objects + +```typescript +import { useQueryStates, parseAsString, parseAsInteger } from 'nuqs'; + +const filterParsers = { + search: parseAsString.withDefault(''), + category: parseAsString.withDefault('all'), + minPrice: parseAsInteger, + maxPrice: parseAsInteger, + page: parseAsInteger.withDefault(1), +}; + +export function useAdvancedFilters() { + const [filters, setFilters] = useQueryStates(filterParsers); + + const updateFilter = <K extends keyof typeof filters>( + key: K, + value: (typeof filters)[K] + ) => { + setFilters({ [key]: value, page: 1 }); // Reset page on filter change + }; + + const resetFilters = () => { + setFilters({ + search: '', + category: 'all', + minPrice: null, + maxPrice: null, + page: 1, + }); + }; + + return { filters, updateFilter, resetFilters }; +} +``` + +### Shallow Routing + +Prevent full page reloads when updating URL state: + +```typescript +const [tab, setTab] = useQueryState('tab', { + shallow: true, // Default is true in nuqs + history: 'push', // or 'replace' +}); +``` + +## React Context Guidelines + +### When to Use Context + +- Theme/appearance settings +- User preferences +- Feature flags +- Cross-cutting concerns (toast notifications, modals) + +### When NOT to Use Context + +- Server data (use React Query instead) +- Form state (use form libraries) +- Single-component state (use useState) +- State that should be in URL + +### Context Pattern + +```typescript +// context/DashboardContext.tsx +import { createContext, useContext, useState, ReactNode } from 'react'; + +interface DashboardState { + sidebarCollapsed: boolean; + activeWidget: string | null; +} + +interface DashboardContextValue extends DashboardState { + toggleSidebar: () => void; + setActiveWidget: (widget: string | null) => void; +} + +const DashboardContext = createContext<DashboardContextValue | null>(null); + +export function DashboardProvider({ children }: { children: ReactNode }) { + const [state, setState] = useState<DashboardState>({ + sidebarCollapsed: false, + activeWidget: null, + }); + + const toggleSidebar = () => { + setState((prev) => ({ + ...prev, + sidebarCollapsed: !prev.sidebarCollapsed, + })); + }; + + const setActiveWidget = (widget: string | null) => { + setState((prev) => ({ ...prev, activeWidget: widget })); + }; + + return ( + <DashboardContext.Provider + value={{ ...state, toggleSidebar, setActiveWidget }} + > + {children} + </DashboardContext.Provider> + ); +} + +export function useDashboard() { + const context = useContext(DashboardContext); + if (!context) { + throw new Error('useDashboard must be used within DashboardProvider'); + } + return context; +} +``` + +### Split Context for Performance + +Separate frequently-changing values to prevent unnecessary re-renders: + +```typescript +// Separate contexts for state and actions +const DashboardStateContext = createContext<DashboardState | null>(null); +const DashboardActionsContext = createContext<DashboardActions | null>(null); + +export function DashboardProvider({ children }: { children: ReactNode }) { + const [state, setState] = useState<DashboardState>(initialState); + + // Memoize actions to prevent re-renders + const actions = useMemo( + () => ({ + toggleSidebar: () => + setState((prev) => ({ + ...prev, + sidebarCollapsed: !prev.sidebarCollapsed, + })), + setActiveWidget: (widget: string | null) => + setState((prev) => ({ ...prev, activeWidget: widget })), + }), + [] + ); + + return ( + <DashboardStateContext.Provider value={state}> + <DashboardActionsContext.Provider value={actions}> + {children} + </DashboardActionsContext.Provider> + </DashboardStateContext.Provider> + ); +} + +// Separate hooks for state and actions +export function useDashboardState() { + const context = useContext(DashboardStateContext); + if (!context) throw new Error('Missing DashboardProvider'); + return context; +} + +export function useDashboardActions() { + const context = useContext(DashboardActionsContext); + if (!context) throw new Error('Missing DashboardProvider'); + return context; +} +``` + +## Context and URL Synchronization + +When state needs to be both in context (for easy access) and URL (for shareability): + +### Pattern: URL as Source of Truth + +```typescript +import { useQueryState } from 'nuqs'; +import { createContext, useContext, ReactNode } from 'react'; + +interface FilterContextValue { + selectedId: string | null; + setSelectedId: (id: string | null) => void; + view: 'grid' | 'list'; + setView: (view: 'grid' | 'list') => void; +} + +const FilterContext = createContext<FilterContextValue | null>(null); + +export function FilterProvider({ children }: { children: ReactNode }) { + // URL state as the single source of truth + const [selectedId, setSelectedId] = useQueryState('selected'); + const [view, setView] = useQueryState('view', { + defaultValue: 'grid' as const, + parse: (v) => (v === 'list' ? 'list' : 'grid'), + }); + + return ( + <FilterContext.Provider + value={{ + selectedId, + setSelectedId, + view, + setView, + }} + > + {children} + </FilterContext.Provider> + ); +} + +export function useFilters() { + const context = useContext(FilterContext); + if (!context) throw new Error('Missing FilterProvider'); + return context; +} +``` + +### Pattern: Sync Context to URL + +When context state needs to be reflected in URL for specific scenarios: + +```typescript +export function useSyncToUrl() { + const { selectedId } = useItemSelection(); // From context + const [, setUrlSelectedId] = useQueryState('selected'); + + // Sync context changes to URL + useEffect(() => { + setUrlSelectedId(selectedId); + }, [selectedId, setUrlSelectedId]); +} +``` + +### Pattern: Initialize Context from URL + +```typescript +export function SelectionProvider({ children }: { children: ReactNode }) { + // Read initial value from URL + const [urlSelectedId] = useQueryState('selected'); + + const [selectedId, setSelectedId] = useState<string | null>( + urlSelectedId // Initialize from URL + ); + + // Keep context in sync with URL changes + useEffect(() => { + setSelectedId(urlSelectedId); + }, [urlSelectedId]); + + return ( + <SelectionContext.Provider value={{ selectedId, setSelectedId }}> + {children} + </SelectionContext.Provider> + ); +} +``` + +## State Debugging + +### React Query DevTools + +```typescript +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; + +function App() { + return ( + <> + <AppContent /> + <ReactQueryDevtools initialIsOpen={false} /> + </> + ); +} +``` + +### Context Debug Component + +```typescript +function DebugContext() { + const state = useDashboardState(); + + if (process.env.NODE_ENV !== 'development') return null; + + return ( + <pre className="fixed bottom-4 right-4 p-2 bg-black/80 text-white text-xs"> + {JSON.stringify(state, null, 2)} + </pre> + ); +} +``` + +## Best Practices + +1. **URL First**: Default to URL state for shareable data +2. **Minimal Context**: Keep context small and focused +3. **Separate Concerns**: Don't mix server state with UI state +4. **Type Everything**: Use TypeScript for all state types +5. **Default Values**: Always provide sensible defaults +6. **Single Source**: Avoid duplicating state across systems + +## Anti-Patterns + +- Storing server data in context (use React Query) +- Using context for form state (use form libraries) +- Deep nesting of providers +- Not memoizing context actions +- Duplicating URL state in useState diff --git a/.trellis/spec/frontend/type-safety.md b/.trellis/spec/frontend/type-safety.md new file mode 100644 index 0000000..97e9010 --- /dev/null +++ b/.trellis/spec/frontend/type-safety.md @@ -0,0 +1,278 @@ +# Type Safety Guidelines + +This document covers TypeScript best practices for maintaining type safety across the frontend application. + +## Core Principles + +1. **Import types from backend, never redefine them** +2. **Use type inference wherever possible** +3. **Avoid type assertions and escape hatches** +4. **Leverage oRPC for end-to-end type safety** + +## Importing Backend Types + +### DO: Import from API Package + +```typescript +// Good: Import types from the API package +import type { User, Order, Product } from '@your-app/api/modules/users/types'; // Replace with your monorepo package path +import type { OrderStatus } from '@your-app/api/modules/orders/types'; // Replace with your monorepo package path +``` + +### DON'T: Redefine Backend Types + +```typescript +// Bad: Redefining types that exist in backend +interface User { + id: string; + name: string; + email: string; +} + +// Bad: Creating parallel type definitions +type OrderStatus = 'pending' | 'processing' | 'completed'; +``` + +## Type Inference from API Client + +### Using `Awaited<ReturnType>` Pattern + +Infer types directly from API client calls to ensure frontend types stay in sync with backend: + +```typescript +import { orpcClient } from '@/lib/orpc'; + +// Infer the response type from the API client +type UsersResponse = Awaited<ReturnType<typeof orpcClient.users.list>>; + +// Infer a single item type from array response +type User = UsersResponse['items'][number]; + +// Infer input types +type CreateUserInput = Parameters<typeof orpcClient.users.create>[0]; +``` + +### Type Inference in Hooks + +```typescript +import { useQuery } from '@tanstack/react-query'; +import { orpcClient } from '@/lib/orpc'; + +// The return type is automatically inferred +export function useUsers() { + return useQuery({ + queryKey: ['users'], + queryFn: () => orpcClient.users.list(), + }); +} + +// For complex transformations, use explicit inference +type UserListData = Awaited<ReturnType<typeof orpcClient.users.list>>; + +export function useFormattedUsers() { + return useQuery({ + queryKey: ['users', 'formatted'], + queryFn: async () => { + const data = await orpcClient.users.list(); + return transformUsers(data); + }, + }); +} +``` + +## Forbidden Patterns + +### NO @ts-expect-error for Custom Fields + +Never use type suppression to access fields that don't exist in the type: + +```typescript +// Bad: Suppressing type errors +// @ts-expect-error - customField exists at runtime +const value = user.customField; + +// Bad: Using any to bypass type checking +const value = (user as any).customField; +``` + +**Solution**: If a field exists at runtime but not in types, update the backend type definition. + +### NO `any` Type in Cache Updates + +React Query cache updates must maintain type safety: + +```typescript +// Bad: Using any in cache updates +queryClient.setQueryData(['users'], (old: any) => { + return old.map((user: any) => /* ... */); +}); + +// Good: Properly typed cache updates +queryClient.setQueryData<UserListData>(['users'], (old) => { + if (!old) return old; + return { + ...old, + items: old.items.map((user) => /* ... */), + }; +}); +``` + +### NO Type Assertions Without Validation + +```typescript +// Bad: Blind type assertion +const user = data as User; + +// Good: Runtime validation with Zod +import { userSchema } from '@your-app/api/modules/users/types'; // Replace with your monorepo package path +const user = userSchema.parse(data); + +// Good: Type guard +function isUser(data: unknown): data is User { + return ( + typeof data === 'object' && + data !== null && + 'id' in data && + 'email' in data + ); +} +``` + +## View Model Types + +When the frontend needs additional computed properties, create view models that extend backend types: + +```typescript +// types/index.ts +import type { Order } from '@your-app/api/modules/orders/types'; // Replace with your monorepo package path + +// Extend backend type with frontend-specific computed properties +export interface OrderViewModel extends Order { + formattedTotal: string; + statusLabel: string; + isEditable: boolean; +} + +// Transform function +export function toOrderViewModel(order: Order): OrderViewModel { + return { + ...order, + formattedTotal: formatCurrency(order.total), + statusLabel: getStatusLabel(order.status), + isEditable: order.status === 'draft', + }; +} +``` + +## Generic Type Patterns + +### API Response Wrapper + +```typescript +// Generic paginated response type +type PaginatedResponse<T> = { + items: T[]; + total: number; + page: number; + pageSize: number; +}; + +// Usage with inference +type UserListResponse = PaginatedResponse<User>; +``` + +### Hook Return Types + +```typescript +// Explicit return type for complex hooks +interface UseOrderActionsReturn { + updateOrder: (id: string, data: UpdateOrderInput) => Promise<void>; + deleteOrder: (id: string) => Promise<void>; + isUpdating: boolean; + isDeleting: boolean; +} + +export function useOrderActions(): UseOrderActionsReturn { + // Implementation +} +``` + +## Working with External Data + +### API Responses + +```typescript +// Always validate external data +import { z } from 'zod'; + +const externalDataSchema = z.object({ + id: z.string(), + value: z.number(), +}); + +async function fetchExternalData() { + const response = await fetch('/api/external'); + const data = await response.json(); + return externalDataSchema.parse(data); +} +``` + +### Local Storage + +```typescript +// Type-safe local storage wrapper +function getStoredValue<T>(key: string, schema: z.ZodType<T>): T | null { + const stored = localStorage.getItem(key); + if (!stored) return null; + + try { + return schema.parse(JSON.parse(stored)); + } catch { + return null; + } +} +``` + +## TypeScript Configuration + +Ensure strict mode is enabled in `tsconfig.json`: + +```json +{ + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true + } +} +``` + +## Common Type Utilities + +```typescript +// Extract array element type +type ArrayElement<T> = T extends (infer E)[] ? E : never; + +// Make specific properties optional +type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; + +// Make specific properties required +type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>; + +// Non-nullable +type NonNullableFields<T> = { + [K in keyof T]: NonNullable<T[K]>; +}; +``` + +## Checklist + +Before committing, verify: + +- [ ] No `@ts-expect-error` or `@ts-ignore` comments added +- [ ] No `any` types in new code +- [ ] All API response types are inferred or imported from backend +- [ ] Cache updates are properly typed +- [ ] External data is validated with Zod schemas diff --git a/.trellis/spec/guides/cross-layer-thinking-guide.md b/.trellis/spec/guides/cross-layer-thinking-guide.md new file mode 100644 index 0000000..aa2f66c --- /dev/null +++ b/.trellis/spec/guides/cross-layer-thinking-guide.md @@ -0,0 +1,417 @@ +# Cross-Layer Thinking Guide + +> **Purpose**: Pre-implementation checklist for features that span multiple layers. +> +> **Core Principle**: 30 minutes of thinking saves 3 hours of debugging. + +--- + +## When to Use This Guide + +Use this guide when your feature: + +- Touches 3+ layers (Server Component, Client Component, oRPC, Database) +- Involves data transformation between layers +- Has real-time or event-driven components +- Receives data from external sources (APIs, webhooks, file uploads) + +--- + +## Pre-Implementation Checklist + +Before writing code, answer these questions: + +### 1. Layer Identification + +**Which layers does this feature touch?** + +- [ ] Server Components (RSC - data fetching, static rendering) +- [ ] Client Components (interactivity, browser APIs, React hooks) +- [ ] API Routes / oRPC Procedures (validation, business logic) +- [ ] Middleware (auth checks, redirects, header manipulation) +- [ ] Database (Drizzle ORM queries, migrations) +- [ ] Server Actions (form handling, progressive enhancement) +- [ ] External Services (third-party APIs, webhooks) + +### 2. Data Flow Direction + +**How does data flow?** + +``` +Read Flow: DB -> Drizzle -> oRPC Handler -> API Response -> React Query -> Component -> UI +Write Flow: UI -> Form/Action -> oRPC Mutation -> Handler -> Drizzle -> DB +SSR Flow: DB -> Drizzle -> oRPC Handler -> Server Component -> HTML -> Client Hydration +``` + +- [ ] Read-only (data flows from DB to UI) +- [ ] Write-only (data flows from UI to DB) +- [ ] Bidirectional (both directions) +- [ ] Server-rendered (data fetched in Server Components) +- [ ] Client-fetched (data fetched via React Query in Client Components) + +### 3. Data Format at Each Layer + +**What format is the data at each boundary?** + +| Layer | Format | Example | +| ---------------- | ----------------------- | ----------------------------------------------- | +| Database | SQL types | `TEXT`, `INTEGER`, `TIMESTAMP`, `JSONB` | +| Drizzle ORM | TypeScript types | `string`, `number`, `Date`, `Record<>` | +| oRPC Handler | Zod-validated objects | `{ id: string, createdAt: Date }` | +| oRPC Response | Serialized JSON | `{ id: "abc", createdAt: "2024-01-01T..." }` | +| React Query | Cached response | Same as oRPC response (deserialized) | +| Server Component | Props (must serialize) | No functions, no Date objects, no class instances | +| Client Component | React state | Component props, hook return values | +| UI | Rendered output | HTML, Tailwind-styled elements | + +### 3.1 Serialization Boundary (CRITICAL!) + +**Design Principle**: Data crossing the Server/Client Component boundary must be serializable. + +| Serializable (OK) | NOT Serializable (WILL BREAK) | +| ----------------------- | --------------------------------- | +| `string`, `number` | `Date` objects | +| `boolean`, `null` | `Map`, `Set` | +| Plain objects, arrays | Functions, class instances | +| `undefined` (as absent) | `BigInt`, `Symbol` | + +**Common serialization trap**: + +```typescript +// BAD - Date objects don't serialize across RSC boundary +async function ItemPage() { + const item = await orpcClient.items.get({ itemId: "123" }); + // item.createdAt might be a Date object from Drizzle + return <ClientItem item={item} />; // Date becomes string or breaks! +} + +// GOOD - Convert to serializable format before passing to Client Component +async function ItemPage() { + const item = await orpcClient.items.get({ itemId: "123" }); + return <ClientItem item={{ + ...item, + createdAt: item.createdAt.toISOString(), // Explicit string conversion + }} />; +} +``` + +### 4. Data Transformation Points + +**Where does format change? Who is responsible?** + +| From | To | Transformer | Location | +| ----------------- | ------------------- | ------------------- | -------------------------- | +| DB timestamp | JS Date | Drizzle ORM | Automatic | +| JS Date | ISO string | oRPC serialization | API response | +| ISO string | Display string | React component | UI layer | +| User input | Validated data | Zod schema | oRPC input validation | +| JSONB column | TypeScript object | Drizzle + cast | Query layer | + +### 5. Boundary Questions (Critical!) + +For each layer boundary, ask: + +**RSC / Client Component Boundary:** + +- What data is the Server Component passing as props? +- Is all of it serializable? (no functions, no Date objects, no Maps) +- Could this data be fetched directly in the Client Component via React Query instead? +- Does the Client Component need to refetch or mutate this data? + +**Client Component / oRPC Boundary:** + +- What format does the oRPC response return? +- How does React Query cache and deserialize it? +- What happens if the response format changes? +- Are query keys consistent for cache invalidation? + +**oRPC Handler / Database Boundary:** + +- Are timestamps handled consistently? (ISO strings vs Date objects) +- Are IDs strings or numbers? +- What about null vs undefined? +- Does Drizzle transform types automatically? +- Are JSONB columns properly cast? + +**Middleware / Route Boundary:** + +- Is auth checked in middleware, oRPC procedure, or both? +- What happens if middleware redirects but the API call continues? +- Are headers properly forwarded in SSR context? + +### 6. Authentication Context + +**Where is auth available?** + +| Layer | Auth Method | Notes | +| ---------------- | -------------------------------------- | --------------------------------------- | +| Middleware | `getSession()` from headers/cookies | Runs before route handler | +| Server Component | `getSession()` or `auth()` helper | Can redirect on the server | +| Client Component | `useSession()` hook | May need loading state | +| oRPC Procedure | `protectedProcedure` middleware | Throws UNAUTHORIZED if no session | +| API Route | `getSession()` from request headers | Manual check needed | + +**Common auth pitfall**: + +```typescript +// BAD - Auth checked in middleware but not in oRPC procedure +// If someone calls the API directly, auth is bypassed! +export const middleware = NextResponse.next(); // auth check here +export const getSecret = publicProcedure.handler(...); // no auth check! + +// GOOD - Auth in oRPC procedure (always enforced) +export const getSecret = protectedProcedure.handler(...); +``` + +### 7. Edge Cases + +- [ ] What if the data is empty/null? +- [ ] What if the database query fails? +- [ ] What if the oRPC call times out? +- [ ] What if a referenced entity doesn't exist? +- [ ] What if the user navigates away mid-mutation? +- [ ] What if React Query returns stale data? +- [ ] What if the user's session expires mid-operation? +- [ ] What if the same mutation fires twice (double-click)? + +--- + +## Common Patterns + +### Pattern A: Server Component Data Fetch + +**Layers**: Server Component -> oRPC Client -> Handler -> Database + +**Data Flow**: + +``` +1. Server Component: Calls oRPC client directly (server-side) +2. oRPC Handler: Validates auth, queries database +3. Drizzle: Returns typed results +4. Server Component: Renders HTML with data +5. Client: Receives pre-rendered HTML +``` + +**Common Issues**: + +- **Serialization**: Server Components can render Date objects directly, but cannot pass them as props to Client Components +- **No cache**: Server-side oRPC calls bypass React Query cache; consider prefetching +- **Waterfall**: Sequential server-side calls create request waterfalls; use `Promise.all` for parallel fetching + +### Pattern B: Client Component with React Query + +**Layers**: Client Component -> React Query -> oRPC Client -> Handler -> Database + +**Data Flow**: + +``` +1. Client Component: Mounts, triggers useQuery +2. React Query: Checks cache, calls oRPC client if stale +3. oRPC Client: Sends HTTP request to API route +4. oRPC Handler: Validates input/auth, queries DB +5. Response: JSON back through React Query +6. Client Component: Re-renders with data +``` + +**Common Issues**: + +- **Loading states**: Must handle `isLoading`, `isError`, `isPending` properly +- **Stale data**: Configure `staleTime` and `gcTime` appropriately +- **Cache invalidation**: Use `orpc.xxx.key()` for consistent invalidation after mutations +- **Enabled flag**: Disable queries when required parameters are missing + +### Pattern C: Mutation with Optimistic Update + +**Layers**: Client Component -> useMutation -> oRPC Client -> Handler -> Database + +**Data Flow**: + +``` +1. User: Triggers action (click, form submit) +2. onMutate: Optimistically update React Query cache +3. oRPC Client: Sends mutation request +4. Handler: Validates, writes to DB +5. onSuccess: Invalidate related queries +6. onError: Rollback optimistic update from snapshot +``` + +**Common Issues**: + +- **Rollback complexity**: Must snapshot all affected queries before optimistic update +- **Type safety**: Cache manipulation needs explicit type annotations +- **Race conditions**: Cancel outgoing refetches before optimistic update (`cancelQueries`) +- **Partial failures**: Batch operations may partially succeed + +### Pattern D: Server Action (Form Handling) + +**Layers**: Form -> Server Action -> oRPC Client / DB -> Revalidate + +**Data Flow**: + +``` +1. User: Submits form +2. Server Action: Receives FormData, validates +3. Action: Calls oRPC client or DB directly +4. Action: Calls revalidatePath/revalidateTag +5. Page: Re-renders with updated data +``` + +**Common Issues**: + +- **Progressive enhancement**: Forms work without JS when using Server Actions +- **Validation**: Validate on both client (UX) and server (security) +- **Redirect vs revalidate**: Choose the right post-action behavior +- **Error handling**: Server Action errors need proper error boundaries + +### Pattern E: Middleware + API Route Auth + +**Layers**: Request -> Middleware -> API Route / oRPC -> Handler + +**Data Flow**: + +``` +1. Request: Arrives at Next.js server +2. Middleware: Checks auth, may redirect to login +3. API Route: Handles oRPC request +4. oRPC Middleware: Validates session (protectedProcedure) +5. Handler: Executes business logic +``` + +**Common Issues**: + +- **Double auth check**: Middleware protects pages, oRPC protects API; both are needed +- **Header forwarding**: SSR requests must forward cookies/headers to oRPC client +- **Middleware scope**: Don't run auth middleware on public assets or API routes that handle their own auth + +--- + +## Lessons from Common Bugs + +| Bug | Root Cause | Prevention | +| ------------------------------- | --------------------------------------------------------- | --------------------------------------------------- | +| `Date` props break hydration | Date objects passed from Server to Client Component | Convert to ISO string before passing as props | +| Stale data after mutation | Forgot to invalidate React Query cache | Always invalidate with `orpc.xxx.key()` in onSuccess | +| Auth bypass on API | Auth only in middleware, not in oRPC procedure | Always use `protectedProcedure` for protected data | +| `BigInt` serialization error | Database returns BigInt, JSON.stringify fails | Cast to number or string before response | +| Query fires with null ID | `enabled` flag not set on conditional queries | Always guard with `enabled: !!requiredParam` | +| Cache key mismatch | Manual query key doesn't match oRPC generated key | Always use `orpc.xxx.key()` or `orpc.xxx.queryKey()` | +| N+1 queries in handler | Fetching related data in a loop | Use `inArray()` for batch queries | +| Hydration mismatch | Server and client render different output (e.g., locale) | Ensure consistent data between server and client | +| Headers not forwarded in SSR | oRPC client doesn't forward cookies in server context | Configure client to forward headers in SSR mode | + +--- + +## Checklist Template + +Copy this for your feature: + +```markdown +## Feature: [Name] + +### Layers Involved + +- [ ] Server Component +- [ ] Client Component +- [ ] oRPC Procedure +- [ ] Middleware +- [ ] Database +- [ ] Server Action +- [ ] External Service + +### Data Flow + +[Describe the flow] + +### Format at Each Layer + +| Layer | Format | +| ----- | ------ | +| ... | ... | + +### Transformation Points + +| From | To | Who | +| ---- | --- | --- | +| ... | ... | ... | + +### Auth Strategy + +- Middleware: [yes/no, what it checks] +- oRPC: [publicProcedure/protectedProcedure/adminProcedure] + +### Edge Cases Considered + +- [ ] Empty/null data +- [ ] Invalid format / serialization +- [ ] Operation failure / timeout +- [ ] User cancellation / navigation +- [ ] Session expiry mid-operation +- [ ] Double submission +``` + +--- + +## Cross-Layer Review Mindset + +### The Comparison Trap + +**Wrong thinking**: "This line wasn't changed, so it must be correct." + +``` +Comparison thinking (surface level): + Before: new Date() -> After: new Date() -> "No change, must be fine" + +Global thinking (design level): + Design intent: ISO strings across RSC boundary -> Current: Date object -> "This is a bug" +``` + +**Key insight**: Review validates "system state is correct", not just "change is correct". + +### Data Outlet Checklist + +Every review must cover ALL data outlets: + +``` +Data Outlets: +|-- oRPC Response (handler -> client) +|-- Server Component Props (RSC -> Client Component) +|-- React Query Cache (shared across components) +|-- URL State (nuqs, searchParams) +|-- Server Action Return (action -> form) +|-- Any external interface +``` + +Ask: **"Is the format correct at EACH outlet?"** + +### Review Three Questions + +Before finishing any cross-layer review: + +1. **Outlet Question**: Have I checked ALL data outlets, not just the "core" one? +2. **Design Question**: Does existing code match design principles? (Not "is the change correct?") +3. **Checklist Question**: Could my checklist itself be wrong? + +### Validation vs Verification + +| Approach | Focus | Risk | +| --------------- | ---------------------------- | ------------------------------------ | +| **Incremental** | "Is this change correct?" | Misses pre-existing bugs | +| **Global** | "Is the system correct now?" | More thorough, catches legacy issues | + +Always prefer global verification for cross-layer features. + +--- + +## When Things Go Wrong + +If you encounter a cross-layer bug: + +1. **Identify the boundary** - Where exactly does it fail? +2. **Log at boundaries** - Add logging before and after each transformation +3. **Check assumptions** - What format did you expect vs what you got? +4. **Test in isolation** - Can you reproduce with a simple test case? +5. **Document the fix** - Add to "Lessons from Common Bugs" table + +--- + +**Language**: All documentation should be written in **English**. diff --git a/.trellis/spec/guides/index.md b/.trellis/spec/guides/index.md new file mode 100644 index 0000000..332dd89 --- /dev/null +++ b/.trellis/spec/guides/index.md @@ -0,0 +1,122 @@ +# Thinking Guides for Next.js Full-Stack Projects + +> **Purpose**: Systematic thinking guides to catch issues before they become bugs. +> +> **Core Philosophy**: 30 minutes of thinking saves 3 hours of debugging. + +--- + +## Why Thinking Guides? + +**Most bugs and tech debt come from "didn't think of that"**, not from lack of skill: + +- Didn't think about what happens at layer boundaries -> cross-layer bugs +- Didn't think about code patterns repeating -> duplicated code everywhere +- Didn't think about edge cases -> runtime errors +- Didn't think about future maintainers -> unreadable code + +These guides help you **ask the right questions before coding**. + +--- + +## Available Thinking Guides + +| Guide | Purpose | When to Use | +| ----------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ | +| [Cross-Layer Thinking](./cross-layer-thinking-guide.md) | Think through data flow across layers | Before implementing features that span 3+ layers | +| [Pre-Implementation Checklist](./pre-implementation-checklist.md) | Verify readiness before coding | Before starting any feature implementation | + +--- + +## Quick Reference: When to Use Which Guide + +### Cross-Layer Issues + +Use [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md) when: + +- [ ] Feature touches 3+ layers (Server Component, Client Component, oRPC, Database) +- [ ] Data format changes between layers +- [ ] Multiple consumers need the same data +- [ ] You're not sure where to put some logic +- [ ] Integrates with external services or third-party APIs + +### Before Writing Code + +Use [Pre-Implementation Checklist](./pre-implementation-checklist.md) when: + +- [ ] About to add a constant or config value +- [ ] About to implement new logic +- [ ] About to define a type or Zod schema +- [ ] About to create a component or hook +- [ ] About to add an oRPC procedure +- [ ] Feels like you've seen similar code before + +--- + +## The Pre-Modification Rule (CRITICAL) + +> **Before changing ANY value, ALWAYS search first!** + +```bash +# Search for the value you're about to change +rg "value_to_change" --type ts + +# Check how many files define this value +rg "CONFIG_NAME" --type ts -c +``` + +This single habit prevents most "forgot to update X" bugs. + +--- + +## Next.js-Specific Layers + +In Next.js full-stack projects with oRPC and Drizzle, these are the typical layers: + +``` +Server Components (RSC - data fetching, static rendering) + | + v +Client Components ('use client' - interactivity, React Query) + | + v +API Routes / oRPC Router (type-safe RPC, middleware, validation) + | + v +Service / Business Logic (shared utilities, domain rules) + | + v +Database Layer (Drizzle ORM, PostgreSQL, migrations) +``` + +Each boundary is a potential source of bugs due to: + +- **Serialization** - Only serializable data crosses the RSC/Client boundary (no functions, no Date objects, no Maps) +- **Type mismatches** - Zod schemas on oRPC may not match what the frontend expects +- **Auth context** - Session availability differs between Server Components, API routes, and middleware +- **Rendering mode** - Server Components vs Client Components have different capabilities and constraints +- **Async timing** - React Query caching, stale data, and race conditions + +--- + +## Core Principles + +1. **Search Before Write** - Always search for existing patterns before creating new ones +2. **Think Before Code** - 5 minutes of checklist saves 50 minutes of debugging +3. **Document Assumptions** - Make implicit assumptions explicit +4. **Verify All Layers** - Changes often need updates in multiple places +5. **Learn From Bugs** - Add lessons to these guides after fixing non-trivial bugs + +--- + +## Contributing + +Found a new "didn't think of that" moment? Add it: + +1. If it's a **general thinking pattern** -> Add to existing guide or create new one +2. If it caused a bug -> Add to "Lessons Learned" section in the relevant guide +3. If it's **project-specific** -> Create a separate project-specific guide + +--- + +**Language**: All documentation should be written in **English**. diff --git a/.trellis/spec/guides/pre-implementation-checklist.md b/.trellis/spec/guides/pre-implementation-checklist.md new file mode 100644 index 0000000..e31f267 --- /dev/null +++ b/.trellis/spec/guides/pre-implementation-checklist.md @@ -0,0 +1,289 @@ +# Pre-Implementation Checklist + +> **Purpose**: Ask the right questions **before** writing code to avoid common architectural mistakes. + +--- + +## Why This Checklist? + +Most code quality issues aren't caught during implementation--they're **designed in** from the start: + +| Problem | Root Cause | Cost | +| -------------------------------------- | --------------------------------------------- | -------------------------- | +| Constants duplicated across 5 files | Didn't ask "will this be used elsewhere?" | Refactoring + testing | +| Same logic repeated in multiple hooks | Didn't ask "does this pattern exist?" | Creating abstraction later | +| Cross-layer type mismatches | Didn't ask "who else consumes this?" | Debugging + fixing | +| Zod schema redefined in frontend | Didn't ask "is this type already exported?" | Inconsistent validation | +| oRPC procedure duplicates existing one | Didn't ask "does a similar endpoint exist?" | API surface bloat | + +**This checklist catches these issues before they become code.** + +--- + +## The Checklist + +### 1. Constants & Configuration + +Before adding any constant or config value: + +- [ ] **Cross-package usage?** Will this value be used in both frontend app and API package? + - If yes -> Put in a shared package (e.g., `@your-app/utils` or `@your-app/config`) + - Example: `MAX_UPLOAD_SIZE` used by both file upload UI and oRPC validation + +- [ ] **Multiple consumers?** Will this value be used in 2+ files within the same package? + - If yes -> Put in a shared constants file for that package + - Example: Don't define `DEBOUNCE_MS = 300` in each hook file + +- [ ] **Magic number?** Is this a hardcoded value that could change? + - If yes -> Extract to named constant with comment explaining why + - Example: `PAGINATION_LIMIT: 50 // oRPC default page size` + +- [ ] **Environment-dependent?** Does this differ between dev/staging/production? + - If yes -> Use environment variables with proper validation + - Example: API URLs, feature flags, third-party API keys + +### 2. Logic & Patterns + +Before implementing any logic: + +- [ ] **Pattern exists?** Search for similar patterns in the codebase first + + ```bash + # Example: Before implementing debounced search + rg "debounce" src/ packages/ + rg "useDebounce" src/ packages/ + ``` + +- [ ] **Will repeat?** Will this exact logic be needed in 2+ places? + - If yes -> Create a shared hook/utility **first**, then use it + - Example: `useDebounce` instead of repeating debounce logic in 5 hooks + +- [ ] **React Query pattern?** Is there an existing query/mutation hook for this data? + - Search before creating: `rg "orpc.items" src/` + - Check if you can extend an existing hook rather than creating a new one + +- [ ] **Server or client?** Does this logic need interactivity? + - If no -> Keep it in a Server Component (default) + - If yes -> Extract only the interactive part into a Client Component + +### 3. Types & Schemas + +Before defining types: + +- [ ] **Zod schema exists?** Is there already a Zod schema for this data shape? + - Check the API module's `types.ts`: `rg "Schema = z.object" packages/api/` + - Derive TypeScript types from Zod schemas with `z.infer<typeof schema>` + - Never manually define a TypeScript interface that duplicates a Zod schema + +- [ ] **Existing type?** Does a similar type already exist? + - Search before creating: `rg "interface.*YourTypeName\|type.*YourTypeName" src/ packages/` + +- [ ] **Cross-layer type?** Is this type used across the oRPC boundary? + - If yes -> Define the Zod schema in the API module's `types.ts`, export the inferred type + - Frontend should import types from the API package, not redefine them + +- [ ] **Derived from client?** Can you infer the type from the oRPC client? + ```typescript + // Prefer this over manually defining types + type ItemResult = Awaited<ReturnType<(typeof orpcClient)["items"]["get"]>>; + ``` + +### 4. UI Components + +Before creating UI components: + +- [ ] **Server or Client Component?** Does this component need: + - Event handlers (onClick, onChange)? -> `'use client'` + - React hooks (useState, useEffect)? -> `'use client'` + - Browser APIs (window, document)? -> `'use client'` + - None of the above? -> Keep as Server Component (default) + +- [ ] **Similar component exists?** Search before creating + - `rg "function.*YourComponent\|export.*YourComponent" src/` + +- [ ] **Visual-logic consistency?** If there's already a visual distinction (icon, color, label) for a concept, does your logic match? + +- [ ] **State lifecycle?** Will this component unmount during normal user flow? + - If yes -> Consider where state should persist (URL params with nuqs, parent, context) + +### 5. API Routes & oRPC Procedures + +Before writing an API route or oRPC procedure: + +- [ ] **Existing procedure?** Does a similar oRPC procedure already exist? + - Check the module's `router.ts`: `rg "Router = {" packages/api/` + - Can you extend an existing procedure rather than creating a new one? + +- [ ] **Correct HTTP method?** + - GET for read operations (queries) + - POST for create operations (mutations) + - PUT/PATCH for update operations + - DELETE for removal operations + +- [ ] **Authentication level?** Which base procedure to use? + - Public data -> `publicProcedure` + - User-specific data -> `protectedProcedure` + - Admin operations -> `adminProcedure` + +- [ ] **Input/output schemas defined?** Both should be Zod schemas in `types.ts` + +### 6. Dependencies + +Before adding a dependency: + +- [ ] **Already installed?** Check `package.json` across all packages + ```bash + rg "\"dependency-name\"" package.json packages/*/package.json + ``` + +- [ ] **Built-in alternative?** Can you use a native API or existing utility instead? + - Example: `structuredClone()` instead of `lodash.cloneDeep` + +- [ ] **Bundle impact?** Will this significantly increase the client bundle? + - If yes -> Consider dynamic import or server-only usage + +--- + +## Quick Decision Tree + +``` +Adding a value/constant? +|-- Used in both app AND api package? -> shared package (@your-app/utils) +|-- Used in 2+ files within same package? -> shared constants file ++-- Single file only? -> Local constant is fine + +Adding logic/behavior? +|-- Similar pattern exists? -> Extend or reuse existing +|-- Will be used in 2+ places? -> Create shared hook/utility first ++-- Single use only? -> Implement directly (but document pattern) + +Adding a type? +|-- Zod schema exists? -> Use z.infer<typeof schema> +|-- Crosses oRPC boundary? -> Define in API types.ts, import elsewhere +|-- Can derive from client? -> Use Awaited<ReturnType<...>> ++-- Local only? -> Define locally + +Adding a component? +|-- Needs interactivity? -> 'use client' +|-- Pure display? -> Server Component (default) ++-- Mix of both? -> Split into Server wrapper + Client interactive part +``` + +--- + +## What to Verify Across Layers + +When implementing a feature that spans Server Component -> API -> Database, verify: + +| Layer | Check | +| ---------------- | ------------------------------------------------------------------ | +| Server Component | Data fetched correctly? Props serializable? No client-only APIs? | +| Client Component | Loading/error states handled? React Query cache invalidated? | +| oRPC Procedure | Input validated? Auth checked? Output schema matches? | +| Database Query | No N+1 queries? Proper indexes? Transactions where needed? | +| Zod Schemas | Input and output schemas consistent? Date/null handling correct? | + +--- + +## Anti-Patterns to Avoid + +### Redefining Backend Types + +```typescript +// DON'T: Manually define types that mirror Zod schemas +interface Item { + id: string; + name: string; + createdAt: Date; +} + +// DO: Import or infer from the source of truth +import type { Item } from "@your-app/api/modules/items/types"; +// or +type Item = Awaited<ReturnType<(typeof orpcClient)["items"]["get"]>>["item"]; +``` + +### Manual Query Keys + +```typescript +// DON'T: Manually construct query keys +queryClient.invalidateQueries({ queryKey: ["items", "list"] }); + +// DO: Use oRPC generated keys +queryClient.invalidateQueries({ queryKey: orpc.items.list.key() }); +``` + +### Unnecessary Client Components + +```typescript +// DON'T: Mark everything as 'use client' +'use client'; +export function ItemCard({ item }) { + return <div>{item.name}</div>; // No interactivity needed! +} + +// DO: Keep as Server Component when possible +export function ItemCard({ item }) { + return <div>{item.name}</div>; +} +``` + +### Fetch in Client Components When Server Would Work + +```typescript +// DON'T: Fetch in Client Component when data could come from Server Component +'use client'; +export function ItemList() { + const { data } = useQuery(orpc.items.list.queryOptions({ input: {} })); + return <ul>{data?.items.map(...)}</ul>; +} + +// DO: Fetch in Server Component, pass as props (when no interactivity needed) +export async function ItemList() { + const data = await orpcClient.items.list({}); + return <ul>{data.items.map(...)}</ul>; +} +``` + +--- + +## When to Use This Checklist + +| Trigger | Action | +| ------------------------------------------ | ------------------------- | +| About to add a constant | Run through Section 1 | +| About to implement logic | Run through Section 2 | +| About to define a type or schema | Run through Section 3 | +| About to create a component | Run through Section 4 | +| About to add an oRPC procedure | Run through Section 5 | +| About to add a dependency | Run through Section 6 | +| Feels like you've seen similar code before | **STOP** and search first | + +--- + +## Relationship to Other Guides + +| Guide | Focus | Timing | +| ------------------------------------------------------------- | ------------------------- | --------------------------- | +| **Pre-Implementation Checklist** (this) | Questions before coding | Before writing code | +| [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md) | Data flow across layers | Complex feature planning | + +**Ideal workflow:** + +1. Read this checklist before coding +2. Use Cross-Layer guide for features spanning multiple layers + +--- + +## Lessons Learned + +| Date | Issue | Lesson | +| ---- | ---------------------------------------------- | --------------------------------------------------------------------- | +| - | Zod schema redefined in frontend and backend | Always derive frontend types from the API package's Zod schemas | +| - | `useQuery` used where Server Component sufficed | Ask "does this need interactivity?" before reaching for React Query | +| - | Manual query keys diverged from oRPC keys | Always use `orpc.xxx.key()` or `orpc.xxx.queryKey()` for cache ops | +| - | Type defined in both app and api package | Cross-boundary types must be defined once in the API module | + +--- + +**Core Principle**: 5 minutes of checklist thinking saves 50 minutes of refactoring. diff --git a/.trellis/spec/shared/code-quality.md b/.trellis/spec/shared/code-quality.md new file mode 100644 index 0000000..406ccee --- /dev/null +++ b/.trellis/spec/shared/code-quality.md @@ -0,0 +1,315 @@ +# Code Quality Guidelines + +> Mandatory code quality rules for all Next.js full-stack applications. + +--- + +## No Non-Null Assertions + +**NEVER** use non-null assertions (`!`). They bypass TypeScript's null checking and lead to runtime errors. + +```typescript +// FORBIDDEN +const name = user!.name; +const value = data!.items![0]!; + +// REQUIRED - Use explicit checks +const user = getUser(); +if (!user) { + throw new Error('User not found'); +} +const name = user.name; + +// REQUIRED - Use optional chaining with fallback +const value = data?.items?.[0] ?? defaultValue; + +// REQUIRED - Use local variable after null check +const project = getProject(id); +if (!project) { + return { success: false, reason: 'Project not found' }; +} +const projectName = project.name; +``` + +--- + +## No `any` Type + +```typescript +// BAD +function process(data: any) { ... } + +// GOOD - Use proper types +function process(data: ProcessInput) { ... } + +// GOOD - Use unknown for truly unknown data +function parseJSON(input: string): unknown { + return JSON.parse(input); +} + +// BAD - any in cache updates +queryClient.setQueryData(['users'], (old: any) => ...); + +// GOOD - Properly typed cache updates +queryClient.setQueryData<UserListData>(['users'], (old) => { + if (!old) return old; + return { ...old, items: old.items.filter((u) => u.id !== deletedId) }; +}); +``` + +--- + +## No `@ts-expect-error` / `@ts-ignore` + +```typescript +// FORBIDDEN +// @ts-expect-error - field exists at runtime +const value = user.customField; + +// @ts-ignore +doSomething(invalidArg); + +// REQUIRED - Fix the type issue at the source +// If a field exists at runtime but not in types, update the type definition. +doSomething(validArg); +``` + +--- + +## No `console.log` + +Use structured logging instead of `console.log`. This applies to both frontend and backend code. + +```typescript +// BAD +console.log('User created:', userId); +console.log('Error:', error); + +// GOOD - Backend: use structured logger +logger.info('user_created', { userId }); +logger.error('operation_failed', { error, operationId }); + +// GOOD - Frontend: remove debug logs before commit +// Use browser dev tools for debugging, not console.log +``` + +**Exception**: `console.warn` and `console.error` are acceptable in frontend code for development-time warnings that will not appear in production. + +--- + +## Import Ordering + +Organize imports in this order, separated by blank lines: + +```typescript +// 1. Node built-ins +import path from 'node:path'; + +// 2. External packages +import { z } from 'zod'; +import { useQuery } from '@tanstack/react-query'; + +// 3. Internal workspace packages +import type { User } from '@your-app/api/modules/users/types'; + +// 4. Local imports (relative paths) +import { formatDate } from './utils'; +import type { Props } from './types'; +``` + +Always use `import type` for type-only imports: + +```typescript +// GOOD +import type { User, Project } from './types'; +import { createUser } from './procedures'; + +// BAD - Mixed imports without type annotation +import { User, createUser } from './types'; +``` + +--- + +## Naming Conventions + +### Files and Directories + +| Type | Convention | Example | +| --------------- | --------------------------- | --------------------------- | +| React Component | PascalCase | `UserProfile.tsx` | +| Hook | camelCase with `use` prefix | `useProject.ts` | +| Utility | kebab-case | `date-utils.ts` | +| Type file | kebab-case or `types.ts` | `types.ts`, `user-types.ts` | +| Test file | Same name + `.test` | `date-utils.test.ts` | +| Directory | kebab-case | `user-profile/` | + +### Variables and Functions + +| Type | Convention | Example | +| -------------- | ------------------------------------------- | ---------------------------------- | +| Variable | camelCase | `userName`, `isActive` | +| Constant | SCREAMING_SNAKE_CASE | `MAX_RETRY_COUNT` | +| Function | camelCase | `getUserById` | +| Class | PascalCase | `UserService` | +| Type/Interface | PascalCase | `UserInput`, `ProjectOutput` | +| Enum | PascalCase (type), SCREAMING_SNAKE (values) | `enum Status { ACTIVE, INACTIVE }` | + +### Boolean Variables + +Use `is`, `has`, `should`, `can` prefixes: + +```typescript +// GOOD +const isLoading = true; +const hasPermission = user.role === 'admin'; +const shouldRefresh = Date.now() > expiresAt; +const canEdit = isOwner || hasPermission; + +// BAD +const loading = true; +const permission = user.role === 'admin'; +``` + +--- + +## Error Handling + +### Never Swallow Errors + +```typescript +// BAD - Silent failure +try { + await dangerousOperation(); +} catch (e) { + // nothing +} + +// GOOD - Log and handle +try { + await dangerousOperation(); +} catch (error) { + logger.error('operation_failed', { error }); + throw new AppError('Operation failed', 'OPERATION_FAILED'); +} +``` + +### Consistent Error Response Format + +All API responses must use the standard `success` + `reason` format: + +```typescript +// Success +return { + success: true, + reason: 'Operation completed successfully', + data: result, +}; + +// Error +return { + success: false, + reason: 'Insufficient permissions to perform this action', +}; +``` + +--- + +## Dead Code Elimination + +- Remove unused imports (Biome enforces this automatically) +- Remove commented-out code blocks +- Remove unused variables, functions, and types +- Remove unreachable code after `return`, `throw`, `break`, `continue` + +```typescript +// BAD - Dead code +function processOrder(order: Order) { + // const oldLogic = order.items.map(...); + const result = newLogic(order); + return result; + cleanup(); // unreachable +} + +// GOOD - Clean +function processOrder(order: Order) { + return newLogic(order); +} +``` + +--- + +## Lint and Type Check Before Commit + +```bash +# MUST pass before every commit +pnpm lint +pnpm type-check + +# Production build check (catches additional issues) +pnpm build + +# Or combined +pnpm lint && pnpm type-check && pnpm build +``` + +--- + +## Testing Guidelines + +### Test File Location + +``` +src/ + __tests__/ # Integration tests + api.test.ts +app/ + feature/ + page.tsx + page.test.tsx # Co-located test (when appropriate) +``` + +### Test Structure (AAA Pattern) + +```typescript +describe('OrderService', () => { + describe('createOrder', () => { + it('should create an order with valid input', async () => { + // Arrange + const input = { items: [{ productId: '1', quantity: 2 }] }; + + // Act + const result = await createOrder(input); + + // Assert + expect(result.success).toBe(true); + expect(result.order.items).toHaveLength(1); + }); + + it('should reject empty order', async () => { + // Arrange + const input = { items: [] }; + + // Act + const result = await createOrder(input); + + // Assert + expect(result.success).toBe(false); + }); + }); +}); +``` + +--- + +## Summary + +| Rule | Reason | +| ------------------------------ | ------------------- | +| No `!` assertions | Runtime errors | +| No `any` type | Type safety | +| No `@ts-expect-error` | Masks real issues | +| No `console.log` | Use structured logs | +| Lint + typecheck before commit | Consistent code | +| Structured errors | Consistent handling | +| Never swallow errors | Debuggability | +| Remove dead code | Maintainability | diff --git a/.trellis/spec/shared/dependencies.md b/.trellis/spec/shared/dependencies.md new file mode 100644 index 0000000..83b176f --- /dev/null +++ b/.trellis/spec/shared/dependencies.md @@ -0,0 +1,173 @@ +# Dependencies & Versions + +> Adjust versions to your project. These represent a known-working combination as of the time of writing. Pin or widen ranges to match your stability requirements. + +--- + +## Runtime Environment + +| Dependency | Version | Description | +|------------|---------|-------------| +| Node.js | >=20 | JavaScript runtime | +| pnpm | ^10.x | Package manager | + +--- + +## Core Framework + +| Package | Version | Description | +|---------|---------|-------------| +| next | ^15.x | React framework for production | +| react | ^19.x | UI library | +| react-dom | ^19.x | React DOM renderer | +| typescript | ^5.x | TypeScript language | + +--- + +## Backend + +### API Layer + +| Package | Version | Description | +|---------|---------|-------------| +| hono | ^4.x | Lightweight web framework | +| @orpc/server | ^1.x | oRPC server implementation | +| @orpc/client | ^1.x | oRPC client | +| @orpc/zod | ^1.x | oRPC Zod integration | +| @orpc/openapi | ^1.x | OpenAPI schema generation | +| zod | ^4.x | Schema validation | + +### Database + +| Package | Version | Description | +|---------|---------|-------------| +| drizzle-orm | ^0.44.x | TypeScript ORM | +| drizzle-kit | ^0.31.x | Drizzle CLI tools | +| drizzle-zod | ^0.8.x | Drizzle + Zod integration | +| pg | ^8.x | PostgreSQL client | + +### Authentication + +| Package | Version | Description | +|---------|---------|-------------| +| better-auth | ^1.x | Authentication library | + +### Caching & Queue + +| Package | Version | Description | +|---------|---------|-------------| +| @upstash/redis | ^1.x | Redis client (Upstash) | +| @upstash/qstash | ^2.x | Message queue | + +--- + +## AI Integration + +| Package | Version | Description | +|---------|---------|-------------| +| ai | ^5.x | Vercel AI SDK core | +| @ai-sdk/react | ^2.x | AI SDK React hooks | +| @ai-sdk/openai | ^2.x | OpenAI provider | +| @ai-sdk/anthropic | ^2.x | Anthropic provider | + +--- + +## Frontend + +### UI Components + +| Package | Version | Description | +|---------|---------|-------------| +| @radix-ui/* | latest | Headless UI primitives | +| lucide-react | ^0.x | Icon library | +| cmdk | ^1.x | Command palette | +| sonner | ^2.x | Toast notifications | + +### Styling + +| Package | Version | Description | +|---------|---------|-------------| +| tailwindcss | ^4.x | Utility-first CSS (v4 config format) | +| @tailwindcss/postcss | ^4.x | PostCSS plugin | +| tailwind-merge | ^3.x | Tailwind class merging | +| class-variance-authority | ^0.7.x | Variant management | +| clsx | ^2.x | Class name utility | + +### State Management + +| Package | Version | Description | +|---------|---------|-------------| +| @tanstack/react-query | ^5.x | Data fetching & caching | +| @orpc/tanstack-query | ^1.x | oRPC + React Query bridge | +| nuqs | ^2.x | URL state management | +| react-hook-form | ^7.x | Form state management | +| @hookform/resolvers | ^5.x | Form validation resolvers | + +### Internationalization + +| Package | Version | Description | +|---------|---------|-------------| +| next-intl | ^4.x | Next.js i18n | + +### Utilities + +| Package | Version | Description | +|---------|---------|-------------| +| date-fns | ^4.x | Date utilities | +| es-toolkit | ^1.x | Utility functions | +| nanoid | ^5.x | ID generation | +| p-limit | ^7.x | Concurrency control | + +--- + +## Monitoring & Logging + +| Package | Version | Description | +|---------|---------|-------------| +| @sentry/nextjs | ^10.x | Error tracking | + +--- + +## Development Tools + +### Build & Bundling + +| Package | Version | Description | +|---------|---------|-------------| +| turbo | ^2.x | Monorepo build system | +| tsx | ^4.x | TypeScript executor | + +### Code Quality + +| Package | Version | Description | +|---------|---------|-------------| +| @biomejs/biome | ^2.x | Linter & formatter | +| husky | ^9.x | Git hooks | + +### Testing + +| Package | Version | Description | +|---------|---------|-------------| +| @playwright/test | ^1.x | E2E testing | + +--- + +## Important Notes + +1. **React 19**: Major version with breaking changes from React 18 +2. **Next.js 15**: App Router is the primary routing pattern +3. **TailwindCSS 4**: Uses the new v4 configuration format (not `tailwind.config.js`) +4. **Zod 4**: Latest version with improved TypeScript support +5. **Monorepo**: Use `@your-app/*` for internal workspace package references + +--- + +## Updating Dependencies + +When updating dependencies: + +1. Check compatibility with React 19 and Next.js 15 +2. Update pnpm overrides if changing React or Drizzle versions +3. Run `pnpm install` from the root directory +4. Run `pnpm type-check` to verify TypeScript compatibility +5. Run `pnpm build` to ensure production build works diff --git a/.trellis/spec/shared/index.md b/.trellis/spec/shared/index.md new file mode 100644 index 0000000..8a87be0 --- /dev/null +++ b/.trellis/spec/shared/index.md @@ -0,0 +1,66 @@ +# Shared Development Guidelines + +> These guidelines apply to all Next.js full-stack applications using this architecture. + +--- + +## Documentation Files + +| File | Description | When to Read | +| -------------------------------------- | ------------------------------------ | ----------------------- | +| [code-quality.md](./code-quality.md) | Code quality mandatory rules | Always | +| [typescript.md](./typescript.md) | TypeScript best practices | Type-related decisions | +| [dependencies.md](./dependencies.md) | Dependency versions and constraints | Adding/updating deps | + +--- + +## Quick Navigation + +| Task | File | +| --------------------------- | -------------------------------------- | +| Code quality rules | [code-quality.md](./code-quality.md) | +| Type annotations | [typescript.md](./typescript.md) | +| Dependency management | [dependencies.md](./dependencies.md) | + +--- + +## Core Rules (MANDATORY) + +| Rule | File | +| ----------------------------------------- | -------------------------------------- | +| No non-null assertions (`!`) | [code-quality.md](./code-quality.md) | +| No `any` type | [code-quality.md](./code-quality.md) | +| No `@ts-expect-error` / `@ts-ignore` | [code-quality.md](./code-quality.md) | +| No `console.log` (use structured logging) | [code-quality.md](./code-quality.md) | +| Zod-first type definitions | [typescript.md](./typescript.md) | +| Import types from backend, never redefine | [typescript.md](./typescript.md) | +| Standard response format (`success` + `reason`) | [typescript.md](./typescript.md) | + +--- + +## Before Every Commit + +- [ ] `pnpm lint` - 0 errors +- [ ] `pnpm type-check` - 0 errors +- [ ] `pnpm build` - production build succeeds +- [ ] No `any` types in new code +- [ ] No non-null assertions (`!`) +- [ ] No `@ts-expect-error` or `@ts-ignore` comments +- [ ] No `console.log` statements (use `logger`) +- [ ] Tests pass (if applicable) + +--- + +## Code Review Checklist + +- [ ] Types are explicit, not `any` +- [ ] API inputs/outputs have Zod schemas +- [ ] Error handling returns structured responses +- [ ] No duplicate type definitions (import from source of truth) +- [ ] Naming follows conventions (files: kebab-case, components: PascalCase) +- [ ] Unused imports and dead code removed +- [ ] No swallowed errors (silent `catch` blocks) + +--- + +**Language**: All documentation must be written in **English**. diff --git a/.trellis/spec/shared/typescript.md b/.trellis/spec/shared/typescript.md new file mode 100644 index 0000000..2ae9276 --- /dev/null +++ b/.trellis/spec/shared/typescript.md @@ -0,0 +1,441 @@ +# TypeScript Best Practices + +> TypeScript guidelines for Next.js full-stack applications. + +--- + +## Zod-First Type Definitions + +Define Zod schemas first, then infer TypeScript types from them. Never define types manually when a Zod schema exists. + +```typescript +import { z } from 'zod'; + +// 1. Define the schema (single source of truth) +export const createUserInputSchema = z.object({ + name: z.string().min(1).max(100), + email: z.string().email(), + role: z.enum(['admin', 'member', 'viewer']), +}); + +export const createUserOutputSchema = z.object({ + success: z.boolean(), + reason: z.string(), + user: z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + }).optional(), +}); + +// 2. Derive types from schemas +export type CreateUserInput = z.infer<typeof createUserInputSchema>; +export type CreateUserOutput = z.infer<typeof createUserOutputSchema>; + +// BAD - Manual type that duplicates schema +interface CreateUserInput { + name: string; + email: string; + role: 'admin' | 'member' | 'viewer'; +} +``` + +### Reusable Base Schemas + +```typescript +const paginationSchema = z.object({ + page: z.number().min(1).default(1), + limit: z.number().min(1).max(100).default(20), +}); + +const timestampSchema = z.object({ + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}); + +// Compose into larger schemas +export const listOrdersInputSchema = paginationSchema.extend({ + status: orderStatusZodSchema.optional(), + customerId: z.string().optional(), +}); +``` + +--- + +## Type Inference from API + +Import types from the backend or infer them from the API client. Never redefine backend types on the frontend. + +### Import from Backend Package + +```typescript +// GOOD - Import from the API package +import type { User, Order } from '@your-app/api/modules/users/types'; +import type { OrderStatus } from '@your-app/api/modules/orders/types'; + +// BAD - Redefining types that exist in backend +interface User { + id: string; + name: string; + email: string; +} +``` + +### Infer from API Client + +```typescript +import { orpcClient } from '@/lib/orpc'; + +// Infer the response type from the API client +type UsersResponse = Awaited<ReturnType<typeof orpcClient.users.list>>; + +// Infer a single item type from array response +type User = UsersResponse['items'][number]; + +// Infer input types +type CreateUserInput = Parameters<typeof orpcClient.users.create>[0]; +``` + +### Type Inference in Hooks + +```typescript +// The return type is automatically inferred from oRPC +export function useUsers() { + return useQuery({ + queryKey: ['users'], + queryFn: () => orpcClient.users.list(), + }); +} + +// For complex transformations, use explicit inference +type UserListData = Awaited<ReturnType<typeof orpcClient.users.list>>; + +export function useFormattedUsers() { + return useQuery({ + queryKey: ['users', 'formatted'], + queryFn: async () => { + const data = await orpcClient.users.list(); + return transformUsers(data); + }, + }); +} +``` + +--- + +## Discriminated Unions + +Use discriminated unions for types that can be one of several shapes. Use strict equality (`=== true`) for narrowing. + +### TypeScript Discriminated Union + +```typescript +type Result<T> = + | { success: true; data: T } + | { success: false; error: string }; + +const result: Result<User> = doSomething(); + +// CORRECT: Use === true for narrowing +if (result.success === true) { + console.log(result.data); // TypeScript knows data exists +} else { + console.log(result.error); // TypeScript knows error exists +} +``` + +### Zod Discriminated Union + +```typescript +export const notificationSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal('email'), + recipient: z.string().email(), + subject: z.string(), + }), + z.object({ + type: z.literal('sms'), + phoneNumber: z.string(), + message: z.string(), + }), + z.object({ + type: z.literal('push'), + deviceToken: z.string(), + title: z.string(), + body: z.string(), + }), +]); + +type Notification = z.infer<typeof notificationSchema>; +``` + +--- + +## Generic Patterns + +### Generic Result Type + +```typescript +type Result<T> = + | { success: true; data: T } + | { success: false; error: string }; + +function createResult<T>(data: T): Result<T> { + return { success: true, data }; +} +``` + +### Generic Paginated Response + +```typescript +type PaginatedResponse<T> = { + items: T[]; + total: number; + page: number; + pageSize: number; +}; + +type UserListResponse = PaginatedResponse<User>; +``` + +### Generic with Constraints + +```typescript +function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { + return obj[key]; +} +``` + +### Common Utility Types + +```typescript +// Extract array element type +type ArrayElement<T> = T extends (infer E)[] ? E : never; + +// Make specific properties optional +type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; + +// Make specific properties required +type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>; +``` + +--- + +## Standard Response Format + +All API responses must include `success` and `reason` fields. + +```typescript +// Output schema pattern +export const operationResultSchema = z.object({ + success: z.boolean(), + reason: z.string(), + data: z.unknown().optional(), +}); + +// Success response +return { + success: true, + reason: 'User created successfully', + user: { id, name, email }, +}; + +// Error response +return { + success: false, + reason: 'Email address is already in use', +}; +``` + +### Batch Operation Response + +```typescript +export const batchOperationResultSchema = z.object({ + success: z.boolean(), + total: z.number(), + processed: z.number(), + failed: z.number(), + errors: z.array(z.object({ + itemId: z.string(), + error: z.string(), + })).optional(), +}); +``` + +--- + +## Forbidden Patterns + +### No `any` + +```typescript +// BAD +function process(data: any) { ... } + +// GOOD +function process(data: unknown) { ... } +function process(data: ProcessInput) { ... } +``` + +### No Non-null Assertion + +```typescript +// BAD +const name = user!.name; +const first = items[0]!; + +// GOOD +if (user) { + const name = user.name; +} + +const first = items[0]; +if (!first) { + return { success: false, reason: 'No items found' }; +} +``` + +### No `@ts-expect-error` / `@ts-ignore` + +```typescript +// BAD +// @ts-expect-error - customField exists at runtime +const value = user.customField; + +// GOOD - Update the type definition instead +interface User { + customField: string; + // ... +} +``` + +### No Type Assertions Without Validation + +```typescript +// BAD - Blind assertion +const user = data as User; + +// GOOD - Runtime validation with Zod +const user = userSchema.parse(data); + +// GOOD - Type guard +function isUser(data: unknown): data is User { + return ( + typeof data === 'object' && + data !== null && + 'id' in data && + 'email' in data + ); +} +``` + +--- + +## Type Imports + +Always use `import type` for type-only imports: + +```typescript +// GOOD +import type { User, Project } from './types'; +import { createUser } from './procedures'; + +// Also acceptable +import { type User, createUser } from './types'; + +// BAD +import { User, createUser } from './types'; +``` + +--- + +## Explicit Return Types for Exports + +Always annotate explicit return types on exported functions: + +```typescript +// BAD - Implicit return type +export function getUser(id: string) { + return db.query.users.findFirst({ where: eq(users.id, id) }); +} + +// GOOD - Explicit return type +export function getUser(id: string): Promise<User | undefined> { + return db.query.users.findFirst({ where: eq(users.id, id) }); +} +``` + +--- + +## TypeScript Configuration + +Ensure strict mode is enabled: + +```json +{ + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true + } +} +``` + +--- + +## Drizzle Type Inference + +```typescript +// Infer types from Drizzle tables +type User = typeof userTable.$inferSelect; +type NewUser = typeof userTable.$inferInsert; + +// Combine with Zod via drizzle-zod +import { createSelectSchema, createInsertSchema } from 'drizzle-zod'; +const userSelectSchema = createSelectSchema(userTable); +const userInsertSchema = createInsertSchema(userTable); +``` + +--- + +## View Model Types + +When the frontend needs computed properties, extend backend types rather than redefining them: + +```typescript +import type { Order } from '@your-app/api/modules/orders/types'; + +export interface OrderViewModel extends Order { + formattedTotal: string; + statusLabel: string; + isEditable: boolean; +} + +export function toOrderViewModel(order: Order): OrderViewModel { + return { + ...order, + formattedTotal: formatCurrency(order.total), + statusLabel: getStatusLabel(order.status), + isEditable: order.status === 'draft', + }; +} +``` + +--- + +## Summary + +| Practice | Reason | +| --------------------------- | ----------------------------- | +| Zod-first types | Single source of truth | +| Import, don't redefine | No type drift | +| `=== true` for unions | Proper narrowing | +| Generics for reuse | DRY, type-safe | +| `success` + `reason` format | Consistent API responses | +| No `any` | Type safety | +| No `!` assertions | Runtime safety | +| No `@ts-expect-error` | Masks real issues | +| `import type` | Clear separation, tree-shake | +| Explicit return types | Documentation, catch errors | diff --git a/.trellis/tasks/00-bootstrap-guidelines/prd.md b/.trellis/tasks/00-bootstrap-guidelines/prd.md new file mode 100644 index 0000000..dbad710 --- /dev/null +++ b/.trellis/tasks/00-bootstrap-guidelines/prd.md @@ -0,0 +1,139 @@ +# Bootstrap Task: Fill Project Development Guidelines + +**You (the AI) are running this task. The developer does not read this file.** + +The developer just ran `trellis init` on this project for the first time. +`.trellis/` now exists with empty spec scaffolding, and this bootstrap task +exists under `.trellis/tasks/`. When they want to work on it, they should start +this task from a session that provides Trellis session identity. + +**Your job**: help them populate `.trellis/spec/` with the team's real +coding conventions. Every future AI session — this project's +`trellis-implement` and `trellis-check` sub-agents — auto-loads spec files +listed in per-task jsonl manifests. Empty spec = sub-agents write generic +code. Real spec = sub-agents match the team's actual patterns. + +Don't dump instructions. Open with a short greeting, figure out if the repo +has any existing convention docs (CLAUDE.md, .cursorrules, etc.), and drive +the rest conversationally. + +--- + +## Status (update the checkboxes as you complete each item) + +- [ ] Fill backend guidelines +- [ ] Fill frontend guidelines +- [ ] Add code examples + +--- + +## Spec files to populate + + +### Backend guidelines + +| File | What to document | +|------|------------------| +| `.trellis/spec/backend/directory-structure.md` | Where different file types go (routes, services, utils) | +| `.trellis/spec/backend/database-guidelines.md` | ORM, migrations, query patterns, naming conventions | +| `.trellis/spec/backend/error-handling.md` | How errors are caught, logged, and returned | +| `.trellis/spec/backend/logging-guidelines.md` | Log levels, format, what to log | +| `.trellis/spec/backend/quality-guidelines.md` | Code review standards, testing requirements | + + +### Frontend guidelines + +| File | What to document | +|------|------------------| +| `.trellis/spec/frontend/directory-structure.md` | Component/page/hook organization | +| `.trellis/spec/frontend/component-guidelines.md` | Component patterns, props conventions | +| `.trellis/spec/frontend/hook-guidelines.md` | Custom hook naming, patterns | +| `.trellis/spec/frontend/state-management.md` | State library, patterns, what goes where | +| `.trellis/spec/frontend/type-safety.md` | TypeScript conventions, type organization | +| `.trellis/spec/frontend/quality-guidelines.md` | Linting, testing, accessibility | + + +### Thinking guides (already populated) + +`.trellis/spec/guides/` contains general thinking guides pre-filled with +best practices. Customize only if something clearly doesn't fit this project. + +--- + +## How to fill the spec + +### Step 1: Import from existing convention files first (preferred) + +Search the repo for existing convention docs. If any exist, read them and +extract the relevant rules into the matching `.trellis/spec/` files — +usually much faster than documenting from scratch. + +| File / Directory | Tool | +|------|------| +| `CLAUDE.md` / `CLAUDE.local.md` | Claude Code | +| `AGENTS.md` | Codex / Claude Code / agent-compatible tools | +| `.cursorrules` | Cursor | +| `.cursor/rules/*.mdc` | Cursor (rules directory) | +| `.windsurfrules` | Windsurf | +| `.clinerules` | Cline | +| `.roomodes` | Roo Code | +| `.github/copilot-instructions.md` | GitHub Copilot | +| `.vscode/settings.json` → `github.copilot.chat.codeGeneration.instructions` | VS Code Copilot | +| `CONVENTIONS.md` / `.aider.conf.yml` | aider | +| `CONTRIBUTING.md` | General project conventions | +| `.editorconfig` | Editor formatting rules | + +### Step 2: Analyze the codebase for anything not covered by existing docs + +Scan real code to discover patterns. Before writing each spec file: +- Find 2-3 real examples of each pattern in the codebase. +- Reference real file paths (not hypothetical ones). +- Document anti-patterns the team clearly avoids. + +### Step 3: Document reality, not ideals + +**Critical**: write what the code *actually does*, not what it should do. +Sub-agents match the spec, so aspirational patterns that don't exist in the +codebase will cause sub-agents to write code that looks out of place. + +If the team has known tech debt, document the current state — improvement +is a separate conversation, not a bootstrap concern. + +--- + +## Quick explainer of the runtime (share when they ask "why do we need spec at all") + +- Every AI coding task spawns two sub-agents: `trellis-implement` (writes + code) and `trellis-check` (verifies quality). +- Each task has `implement.jsonl` / `check.jsonl` manifests listing which + spec files to load. +- The platform hook auto-injects those spec files + the task's `prd.md` + into every sub-agent prompt, so the sub-agent codes/reviews per team + conventions without anyone pasting them manually. +- Source of truth: `.trellis/spec/`. That's why filling it well now pays + off forever. + +--- + +## Completion + +When the developer confirms the checklist items above are done with real +examples (not placeholders), guide them to run: + +```bash +python3 ./.trellis/scripts/task.py finish +python3 ./.trellis/scripts/task.py archive 00-bootstrap-guidelines +``` + +After archive, every new developer who joins this project will get a +`00-join-<slug>` onboarding task instead of this bootstrap task. + +--- + +## Suggested opening line + +"Welcome to Trellis! Your init just set me up to help you fill the project +spec — a one-time setup so every future AI session follows the team's +conventions instead of writing generic code. Before we start, do you have +any existing convention docs (CLAUDE.md, .cursorrules, CONTRIBUTING.md, +etc.) I can pull from, or should I scan the codebase from scratch?" diff --git a/.trellis/tasks/00-bootstrap-guidelines/task.json b/.trellis/tasks/00-bootstrap-guidelines/task.json new file mode 100644 index 0000000..1f79863 --- /dev/null +++ b/.trellis/tasks/00-bootstrap-guidelines/task.json @@ -0,0 +1,29 @@ +{ + "id": "00-bootstrap-guidelines", + "name": "00-bootstrap-guidelines", + "title": "Bootstrap Guidelines", + "description": "Fill in project development guidelines for AI agents", + "status": "in_progress", + "dev_type": "docs", + "scope": null, + "package": null, + "priority": "P1", + "creator": "TalexDreamSoul", + "assignee": "TalexDreamSoul", + "createdAt": "2026-07-01", + "completedAt": null, + "branch": null, + "base_branch": null, + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [ + ".trellis/spec/backend/", + ".trellis/spec/frontend/" + ], + "notes": "First-time setup task created by trellis init (fullstack project)", + "meta": {} +} \ No newline at end of file diff --git a/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/check.jsonl b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/check.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-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-01-nextjs-fullstack-crud-rbac-audit/design.md b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/design.md new file mode 100644 index 0000000..d31725f --- /dev/null +++ b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/design.md @@ -0,0 +1,113 @@ +# Design + +## Architecture + +This task keeps the app in a single Next.js project and adds a small server-side domain layer inside `modules/`: + +- `modules/core/server/`: server-only persistence, session, permissions, and audit helpers. +- `modules/auth/`: UI and client interactions for setup, login, logout, and session state. +- `modules/elders/`: elder schemas/types, server actions or API client helpers, and UI components. +- `modules/settings/`: role/account/audit display components. +- `app/api/.../route.ts`: Route Handlers for auth, session, elders, accounts, and audit APIs. + +The persistence boundary should be centralized behind helper functions that read/write JSON files. UI and route handlers must not know file paths directly. + +## Data Storage + +Use a JSON file store under `.data/teatea.json` by default. The file should contain: + +```ts +type AppData = { + accounts: Account[]; + sessions: Session[]; + elders: Elder[]; + auditLogs: AuditLog[]; +}; +``` + +The store module owns initialization, read, write, and update operations. Updates should read the current snapshot, apply a synchronous mutation callback, and write the full file back. This is enough for the MVP and gives a clear future replacement boundary for Drizzle/Postgres. + +## API Contracts + +Route Handlers return a consistent response shape: + +```ts +type ApiResult<T> = + | ({ success: true; reason: string } & T) + | { success: false; reason: string }; +``` + +Planned endpoints: + +- `GET /api/auth/bootstrap`: returns whether setup is required. +- `POST /api/auth/setup`: creates the first admin account, creates a session, logs setup. +- `POST /api/auth/login`: validates credentials, creates a session, logs login. +- `POST /api/auth/logout`: deletes current session cookie/session, logs logout. +- `GET /api/auth/session`: returns current account and permissions. +- `GET /api/elders`: lists elder profiles. +- `POST /api/elders`: creates an elder profile. +- `PATCH /api/elders/[id]`: updates an elder profile. +- `DELETE /api/elders/[id]`: deletes an elder profile. +- `GET /api/settings/accounts`: lists accounts for authorized roles. +- `GET /api/settings/roles`: lists built-in role definitions. +- `GET /api/audit-logs`: lists recent audit events. + +## Authentication + +Sessions use an HTTP-only cookie. Route Handlers read and write cookies with `await cookies()` from `next/headers`, matching current Next.js behavior. + +Passwords are never stored in plain text. For the MVP, use Node built-in crypto with a per-account salt and `scryptSync` or `scrypt` for password hashing. This avoids adding a dependency while keeping the current local milestone meaningfully better than browser-only auth. + +## Permissions + +Define built-in role permissions in one server/shared constants module: + +- `account:read` +- `account:manage` +- `audit:read` +- `elder:read` +- `elder:create` +- `elder:update` +- `elder:delete` + +Route Handlers call a shared `requirePermission(permission)` helper. This helper returns an authenticated context or a structured forbidden/unauthorized result and writes denied audit entries. + +## Audit Logging + +Audit logging is implemented as a server helper that appends immutable records to the store: + +- timestamp +- actor account ID/email if known +- action +- target type +- target ID +- result: `success` or `denied` or `failure` +- reason + +Audit writes are part of the route handler flow. The MVP accepts best-effort logging for logout when a session has already expired. + +## UI Flow + +Protected app pages should use server session state where practical instead of a client-only `AuthGate`. The app layout can fetch session data and redirect unauthenticated users before rendering protected content. + +The elder page becomes a real data view: + +- Server Component loads initial elders. +- Client component handles create/edit/delete forms and refreshes after mutations. +- Controls are disabled or hidden based on current permissions. + +The settings page becomes a server-loaded administrative view: + +- Role definitions table. +- Accounts table. +- Recent audit log table. + +## Compatibility + +Static module pages not in scope remain untouched except for auth/layout integration. Existing visual design should be preserved: dense operational screens, restrained cards/tables, and Tailwind/Radix-compatible UI components. + +## Risks and Rollback + +- File persistence is not safe for high-concurrency production writes. This is acceptable for MVP and documented as a migration boundary. +- Replacing `AuthGate` with server-side auth can affect all `/app` routes. Rollback point: keep the old component until server session redirect is working. +- Password hashing must use Node runtime APIs, so affected Route Handlers should run in the Node runtime if needed. diff --git a/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/implement.jsonl b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/implement.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-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-01-nextjs-fullstack-crud-rbac-audit/implement.md b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/implement.md new file mode 100644 index 0000000..726ad45 --- /dev/null +++ b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/implement.md @@ -0,0 +1,89 @@ +# Implementation Plan + +## Checklist + +1. Add core server modules: + - JSON store read/write helpers. + - Account/session/password helpers. + - Role and permission definitions. + - Audit logging helper. + - Shared API response helpers. + +2. Add auth APIs: + - `GET /api/auth/bootstrap` + - `POST /api/auth/setup` + - `POST /api/auth/login` + - `POST /api/auth/logout` + - `GET /api/auth/session` + +3. Migrate auth UI: + - Update `AuthPanel` to call server APIs. + - Update `SignOutButton` to call logout API. + - Replace or bypass localStorage-only `AuthGate` with server session protection in the app layout. + - Keep unauthenticated redirects and setup redirects working. + +4. Add elder APIs and UI: + - Create elder types and input validators. + - Implement list/create/update/delete Route Handlers. + - Replace `app/(app)/app/elders/page.tsx` static content with server-loaded CRUD UI. + +5. Add settings/audit UI: + - Implement settings/account, role, and audit APIs. + - Replace `app/(app)/app/settings/page.tsx` static content with server-loaded tables. + +6. Wire audit events: + - Account setup/create. + - Login/logout. + - Elder create/update/delete. + - Denied permission checks. + +7. Verification: + - Run `pnpm lint`. + - Run `pnpm type-check`. + - Run `pnpm build`. + - Start dev server and manually exercise setup/login/CRUD/settings if build passes. + +## Files Expected to Change + +- `modules/auth/components/AuthPanel.tsx` +- `modules/auth/components/AuthGate.tsx` +- `modules/auth/components/SignOutButton.tsx` +- `app/(app)/app/layout.tsx` +- `app/(app)/app/elders/page.tsx` +- `app/(app)/app/settings/page.tsx` + +## Files Expected to Be Created + +- `modules/core/server/store.ts` +- `modules/core/server/auth.ts` +- `modules/core/server/permissions.ts` +- `modules/core/server/audit.ts` +- `modules/core/server/api.ts` +- `modules/elders/types.ts` +- `modules/elders/components/EldersClient.tsx` +- `modules/settings/components/SettingsOverview.tsx` +- `app/api/auth/bootstrap/route.ts` +- `app/api/auth/setup/route.ts` +- `app/api/auth/login/route.ts` +- `app/api/auth/logout/route.ts` +- `app/api/auth/session/route.ts` +- `app/api/elders/route.ts` +- `app/api/elders/[id]/route.ts` +- `app/api/settings/accounts/route.ts` +- `app/api/settings/roles/route.ts` +- `app/api/audit-logs/route.ts` + +## Validation Commands + +```bash +pnpm lint +pnpm type-check +pnpm build +pnpm dev +``` + +## Rollback Points + +- If server auth blocks all app routes, revert only layout/AuthGate changes while keeping APIs. +- If elder CRUD UI is unstable, keep APIs and temporarily render a read-only server table. +- If file store causes build/runtime issues, move the data directory to `/tmp` behind the same store API for verification, then restore `.data` once filesystem assumptions are fixed. diff --git a/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/prd.md b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/prd.md new file mode 100644 index 0000000..4a520f9 --- /dev/null +++ b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/prd.md @@ -0,0 +1,104 @@ +# Next.js Full-Stack CRUD, RBAC, and Audit Logs + +## Goal + +Turn the current static/local-storage Next.js app into a minimal real full-stack application for the first operational slice: account setup/login, built-in RBAC, elder profile CRUD, and auditable administrative actions. + +The first release should remove mock/local-only behavior from the core flow and persist data through server-side APIs so the app can be exercised end-to-end without browser localStorage state. + +## Confirmed Facts + +- The repository is already a Next.js 15 App Router project with routes under `app/`. +- Protected app screens are currently guarded by `modules/auth/components/AuthGate.tsx`, which reads localStorage through `modules/auth/lib/local-auth.ts`. +- Login/register/setup UI exists in `modules/auth/components/AuthPanel.tsx`, but password input is not validated server-side and accounts are browser-local. +- The "老人档案" route at `app/(app)/app/elders/page.tsx` is currently a static `ModulePage`. +- The "权限设置" route at `app/(app)/app/settings/page.tsx` is currently a static `ModulePage`. +- The project has no installed database/auth dependencies such as Drizzle, PostgreSQL client, oRPC, Zod, or better-auth. +- Current `package.json` already supports `pnpm lint`, `pnpm type-check`, and `pnpm build`. + +## MVP Scope + +- Implement a real server-side persistence layer using JSON files under a server-owned data directory for this milestone. This is not mock data: API mutations must write durable data on disk during local/runtime execution. +- Use Next.js Route Handlers for the first API surface instead of adding oRPC/Drizzle/better-auth in this milestone. +- Replace localStorage authentication with server-side account/session APIs and HTTP-only cookie sessions. +- Provide built-in roles and permission groups without user-defined custom role creation in this milestone. +- Implement full CRUD for elder profiles as the first business entity. +- Implement a permissions/settings page that shows accounts, roles, permission coverage, and audit logs from server data. +- Record audit log entries for login, logout, account creation, elder create/update/delete, and permission-sensitive denied actions. + +## Requirements + +### Authentication + +- Setup must create the first administrator account on the server. +- Login must validate account credentials on the server and set an HTTP-only session cookie. +- Logout must clear the session cookie and record an audit event when possible. +- App routes must no longer depend on localStorage for authentication. + +### Roles and Permissions + +- The system must include built-in roles: + - `admin`: full access to accounts, permissions, audit logs, and elder CRUD. + - `manager`: elder CRUD and audit log viewing, but no account/role administration. + - `caregiver`: elder read/update access for care-facing fields, no delete or account administration. + - `viewer`: read-only elder access. +- Permission checks must run on the server for all protected APIs. +- UI navigation or controls may hide unavailable actions, but hidden UI must not be the only enforcement. + +### Elder CRUD + +- Users with permission can list, create, update, and delete elder profiles. +- Elder records must include at minimum: name, gender, birth date or age, care level, room/bed, status, primary contact, phone, medical notes, created/updated timestamps. +- The elder page must show real server data and support create/edit/delete interactions. +- Invalid input must return structured API errors and show usable feedback in the UI. + +### Audit Logs + +- Audit logs must be persisted server-side and visible in the settings page. +- Each audit record must include timestamp, actor account ID/email when available, action, target type, target ID when available, result, and human-readable reason. +- Failed permission checks must be logged without exposing sensitive internals to the client. + +### Data and API + +- API responses must use a consistent `{ success, reason, ... }` shape. +- Server-side data helpers must avoid browser APIs. +- All file persistence operations must be centralized so future Drizzle/Postgres migration has a single boundary to replace. +- Do not use hard-coded UI counters for the implemented CRUD/settings/audit areas once server data exists. + +### Quality + +- Keep TypeScript strict without `any`, non-null assertions, or `@ts-ignore`/`@ts-expect-error`. +- Prefer Server Components for initial data loading and small Client Components only for forms/mutations. +- `pnpm lint`, `pnpm type-check`, and `pnpm build` should pass before marking implementation complete. + +## Acceptance Criteria + +- [ ] First-run setup creates a server-persisted admin account and redirects into the app. +- [ ] Login/logout uses server APIs and an HTTP-only cookie session, not localStorage. +- [ ] Visiting `/app/elders` while authenticated loads elder records from the server persistence layer. +- [ ] An authorized user can create, edit, and delete elder records from the UI, and changes survive page reloads. +- [ ] Unauthorized role attempts against protected elder/account/audit APIs return a forbidden response and create audit log entries. +- [ ] `/app/settings` displays built-in roles, account list, and recent audit log entries from server data. +- [ ] Audit logs are written for account creation, login, logout, elder create/update/delete, and denied permission checks. +- [ ] Existing static module pages outside the MVP continue to render. +- [ ] `pnpm lint` passes. +- [ ] `pnpm type-check` passes. +- [ ] `pnpm build` passes. + +## Out of Scope + +- PostgreSQL/Drizzle migration. +- oRPC adoption. +- better-auth adoption. +- Password reset, email verification, OAuth, multi-factor authentication, or account invitations. +- Custom role builder UI. +- CRUD implementation for every module in the sidebar. +- Production-grade password hashing beyond Node built-in cryptographic hashing suitable for this local MVP. + +## Open Questions + +- None blocking. The MVP assumes "basic CRUD" means the first core business entity, elder profiles, plus real account/session/audit support. + +## Notes + +- Next.js documentation confirms App Router Route Handlers support `GET`, `POST`, `PATCH`, and `DELETE`, `Response.json`, `request.json()`, and async `cookies()` from `next/headers` for cookie reads/writes in current versions. diff --git a/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/task.json b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/task.json new file mode 100644 index 0000000..5d4397a --- /dev/null +++ b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/task.json @@ -0,0 +1,26 @@ +{ + "id": "nextjs-fullstack-crud-rbac-audit", + "name": "nextjs-fullstack-crud-rbac-audit", + "title": "Next.js Full-Stack CRUD, RBAC, and Audit Logs", + "description": "", + "status": "in_progress", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "TalexDreamSoul", + "assignee": "TalexDreamSoul", + "createdAt": "2026-07-01", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/.trellis/workflow.md b/.trellis/workflow.md new file mode 100644 index 0000000..5f70366 --- /dev/null +++ b/.trellis/workflow.md @@ -0,0 +1,708 @@ +# Development Workflow + +--- + +## Core Principles + +1. **Plan before code** — figure out what to do before you start +2. **Specs injected, not remembered** — guidelines are injected via hook/skill, not recalled from memory +3. **Persist everything** — research, decisions, and lessons all go to files; conversations get compacted, files don't +4. **Incremental development** — one task at a time +5. **Capture learnings** — after each task, review and write new knowledge back to spec + +--- + +## Trellis System + +### Developer Identity + +On first use, initialize your identity: + +```bash +python3 ./.trellis/scripts/init_developer.py <your-name> +``` + +Creates `.trellis/.developer` (gitignored) + `.trellis/workspace/<your-name>/`. + +### Spec System + +`.trellis/spec/` holds coding guidelines organized by package and layer. + +- `.trellis/spec/<package>/<layer>/index.md` — entry point with **Pre-Development Checklist** + **Quality Check**. Actual guidelines live in the `.md` files it points to. +- `.trellis/spec/guides/index.md` — cross-package thinking guides. + +```bash +python3 ./.trellis/scripts/get_context.py --mode packages # list packages / layers +``` + +**When to update spec**: new pattern/convention found · bug-fix prevention to codify · new technical decision. + +### Task System + +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 +python3 ./.trellis/scripts/task.py create "<title>" [--slug <name>] [--parent <dir>] +python3 ./.trellis/scripts/task.py start <name> # set active task (session-scoped when available) +python3 ./.trellis/scripts/task.py current --source # show active task and source +python3 ./.trellis/scripts/task.py finish # clear active task (triggers after_finish hooks) +python3 ./.trellis/scripts/task.py archive <name> # move to archive/{year-month}/ +python3 ./.trellis/scripts/task.py list [--mine] [--status <s>] +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 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> + +# Task metadata +python3 ./.trellis/scripts/task.py set-branch <name> <branch> +python3 ./.trellis/scripts/task.py set-base-branch <name> <branch> # PR target +python3 ./.trellis/scripts/task.py set-scope <name> <scope> + +# Hierarchy (parent/child) +python3 ./.trellis/scripts/task.py add-subtask <parent> <child> +python3 ./.trellis/scripts/task.py remove-subtask <parent> <child> + +# PR creation +python3 ./.trellis/scripts/task.py create-pr [name] [--dry-run] +``` + +> Run `python3 ./.trellis/scripts/task.py --help` to see the authoritative, up-to-date list. + +**Current-task mechanism**: `task.py create` creates the task directory and (when session identity is available) auto-sets the per-session active-task pointer so the planning breadcrumb fires immediately. `task.py start` writes the same pointer (idempotent if already set) and flips `task.json.status` from `planning` to `in_progress`. State is stored under `.trellis/.runtime/sessions/`. If no context key is available from hook input, `TRELLIS_CONTEXT_ID`, or a platform-native session environment variable, there is no active task and `task.py start` fails with a session identity hint. `task.py finish` deletes the current session file (status unchanged). `task.py archive <task>` writes `status=completed`, moves the directory to `archive/`, and deletes any runtime session files that still point at the archived task. + +### Workspace System + +Records every AI session for cross-session tracking under `.trellis/workspace/<developer>/`. + +- `journal-N.md` — session log. **Max 2000 lines per file**; a new `journal-(N+1).md` is auto-created when exceeded. +- `index.md` — personal index (total sessions, last active). + +```bash +python3 ./.trellis/scripts/add_session.py --title "Title" --commit "hash" --summary "Summary" +``` + +### Context Script + +```bash +python3 ./.trellis/scripts/get_context.py # full session runtime +python3 ./.trellis/scripts/get_context.py --mode packages # available packages + spec layers +python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed guide for a workflow step +``` + +--- + +<!-- + WORKFLOW-STATE BREADCRUMB CONTRACT (read this before editing the tag blocks below) + + 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 + inject-workflow-state.js (OpenCode plugin) only parse them — there is no + fallback dict baked into the scripts after v0.5.0-rc.0. + + STATUS charset: [A-Za-z0-9_-]+. When the hook can't find a tag, it + degrades to a generic "Refer to workflow.md for current step." line — + intentionally visible so users notice and fix a broken workflow.md. + + INVARIANT (test/regression.test.ts): + 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 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: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 + pointer (block kept for a future + explicit in_progress→completed + transition) + + Editing checklist: + - When you change a [workflow-state:STATUS] block, also check the + matching phase's `[required · once]` walkthrough steps for sync + - Run `trellis update` after editing to push the new bodies to + downstream user projects (block-level managed replacement) + - Full runtime contract: + .trellis/spec/cli/backend/workflow-state-contract.md +--> + +## Phase Index + +``` +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. 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]` (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, ZCode, Reasonix (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 `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 jsonl curation is skipped — + the inline workflow loads `trellis-before-dev` instead of injecting JSONL + into a sub-agent. --> + +[workflow-state:planning-inline] +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 +- 2.1 Implement `[required · repeatable]` +- 2.2 Quality check `[required · repeatable]` +- 2.3 Rollback `[on demand]` + +<!-- Per-turn breadcrumb: shown while status='in_progress'. + 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 class-2 Codex/Gemini/Qoder/Copilot/ZCode/Reasonix/Trae and `trellis-research`: every dispatch prompt starts with `Active task: <task path from task.py current>` before role-specific 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: 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 + codex.dispatch_mode=inline. Codex-only opt-in alternate to + [workflow-state:in_progress]. The main session edits code directly + instead of dispatching sub-agents. --> + +[workflow-state:in_progress-inline] +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.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 + resolver loses the pointer and the hook never fires on archived tasks. + Block preserved for a future status-transition redesign (e.g. an + explicit in_progress→completed command). Edit through the same spec + channel as the live blocks. --> + +[workflow-state:completed] +Code committed. Run `/trellis:finish-work`; if dirty, return to Phase 3.4 first. +[/workflow-state:completed] + +### Rules + +1. Identify which Phase you're in, then continue from the next step there +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. + +### Active Task Routing + +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, ZCode, Reasonix, Trae] + +- 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`. + +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae] + +[codex-inline, Kilo, Antigravity, Devin] + +- 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`. + +[/codex-inline, Kilo, Antigravity, Devin] + +### Guardrails + +- 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 + +At each step, run this to fetch detailed guidance: + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase --step <step> +# e.g. python3 ./.trellis/scripts/get_context.py --mode phase --step 1.1 +``` + +--- + +## Phase 1: Plan + +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 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> +``` + +`--slug` is the human-readable name only. Do **not** include the `MM-DD-` date prefix; `task.py create` adds that prefix automatically. + +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. + +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. + +#### 1.1 Requirement exploration `[required · repeatable]` + +Load the `trellis-brainstorm` skill and explore requirements interactively with the user per the skill's guidance. + +The brainstorm skill will guide you to: +- Ask one question at a time +- 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 + +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, ZCode, Reasonix, Trae] + +Spawn the research sub-agent: + +- **Agent type**: `trellis-research` +- **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, ZCode, Reasonix, Trae] + +[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.) + +[/codex-inline, Kilo, Antigravity, Devin] + +**Research artifact conventions**: +- One file per research topic (e.g. `research/auth-library-comparison.md`) +- Record third-party library usage examples, API references, version constraints in files +- Note relevant spec file paths you discovered for later reference + +Brainstorm and research can interleave freely — pause to research a technical question, then return to talk with the user. + +**Key principle**: Research output must be written to files, not left only in the chat. Conversations get compacted; files don't. + +#### 1.3 Configure context `[required · once]` + +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae] + +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). + +**Format**: one JSON object per line — `{"file": "<path>", "reason": "<why>"}`. Paths are repo-root relative. + +**What to put in**: +- **Spec files** — `.trellis/spec/<package>/<layer>/index.md` and any specific guideline files (`error-handling.md`, `conventions.md`, etc.) relevant to this task +- **Research files** — `{TASK_DIR}/research/*.md` that the sub-agent will need to consult + +**What NOT to put in**: +- Code files (`src/**`, `packages/**/*.ts`, etc.) — those are read by the sub-agent during implementation, not pre-registered here +- Files you're about to modify — same reason + +**Split between the two files**: +- `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 +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Lists every package + its spec layers with paths. Pick the entries that match this task's domain. + +**How to append entries**: + +Either edit the jsonl file directly in your editor, or use: + +```bash +python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" implement "<path>" "<reason>" +python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" check "<path>" "<reason>" +``` + +Delete the seed `_example` line once real entries exist (optional — it's skipped automatically by consumers). + +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. + +Skip this step only when both files already have real curated entries. + +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae] + +[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, Devin] + +#### 1.4 Activate task `[required · once]` + +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. + +#### 1.5 Completion criteria + +| Condition | Required | +|------|:---:| +| `prd.md` exists | ✅ | +| User confirms task should enter implementation | ✅ | +| `task.py start` has been run (status = in_progress) | ✅ | +| `research/` has artifacts (complex tasks) | recommended | +| `design.md` exists (complex tasks) | ✅ | +| `implement.md` exists (complex tasks) | ✅ | + +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae] + +| `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, ZCode, Reasonix, Trae] + +--- + +## Phase 2: Execute + +Goal: turn reviewed planning artifacts into code that passes quality checks. + +#### 2.1 Implement `[required · repeatable]` + +[Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi] + +Spawn the implement sub-agent: + +- **Agent type**: `trellis-implement` +- **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 hook/plugin auto-handles: +- 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 + +[/Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi] + +[codex-sub-agent, Gemini, Qoder, Copilot, ZCode, Reasonix, Trae] + +Spawn the implement sub-agent: + +- **Agent type**: `trellis-implement` +- **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 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, ZCode, Reasonix, Trae] + +[Kiro] + +Spawn the implement sub-agent: + +- **Agent type**: `trellis-implement` +- **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 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, Devin] + +1. Load the `trellis-before-dev` skill to read project guidelines +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 reviewed artifacts +5. Run project lint and type-check + +[/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, ZCode, Reasonix, Trae] + +Spawn the check sub-agent: + +- **Agent type**: `trellis-check` +- **Task description**: Review all code changes against specs and task artifacts; 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`. + +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, ZCode, Reasonix, Trae] + +[codex-inline, Kilo, Antigravity, Devin] + +Load the `trellis-check` skill and verify the code per its guidance: +- Spec compliance +- lint / type-check / tests +- Cross-layer consistency (when changes span layers) + +If issues are found → fix → re-check, until green. + +[/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]` + +- `check` reveals a prd defect → return to Phase 1, fix `prd.md`, then redo 2.1 +- Implementation went wrong → revert code, redo 2.1 +- Need more research → research (same as Phase 1.2), write findings into `research/` + +--- + +## Phase 3: Finish + +Goal: ensure code quality, capture lessons, record the work. + +#### 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: +- Classify the root cause +- Explain why earlier fixes failed +- Propose prevention + +The goal is to capture debugging lessons so the same class of issue doesn't recur. + +#### 3.3 Spec update `[required · once]` + +Load the `trellis-update-spec` skill and review whether this task produced new knowledge worth recording: +- Newly discovered patterns or conventions +- Pitfalls you hit +- New technical decisions + +Update the docs under `.trellis/spec/` accordingly. Even if the conclusion is "nothing to update", walk through the judgment. + +#### 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**: + +1. **Inspect dirty state**: + ```bash + git status --porcelain + ``` + Snapshot every dirty path. If the working tree is clean, skip to 3.5. + +2. **Learn commit style** from recent history (so drafted messages blend in): + ```bash + git log --oneline -5 + ``` + Note the prefix convention (`feat:` / `fix:` / `chore:` / `docs:` ...), language (中文/English), and length style. + +3. **Classify dirty files into two groups**: + - **AI-edited this session** — files you wrote/edited via Edit/Write/Bash tool calls in this session. You know what changed and why. + - **Unrecognized** — dirty files you did NOT touch this session (could be the user's manual edits, leftover WIP from a previous session, or unrelated work). Do NOT silently include these. + +4. **Draft a commit plan**. Group AI-edited files into logical commits (1 commit per coherent change unit, not 1 commit per file). Each entry: `<commit message>` + file list. List unrecognized files separately at the bottom. + +5. **Present the plan once, ask for one-shot confirmation**. Format: + ``` + Proposed commits (in order): + 1. <message> + - <file> + - <file> + 2. <message> + - <file> + + Unrecognized dirty files (NOT in any commit — confirm include/exclude): + - <file> + - <file> + + Reply 'ok' / '行' to execute. Reply with edits, or '我自己来' / 'manual' to abort. + ``` + +6. **On confirmation**: run `git add <files>` + `git commit -m "<msg>"` for each batch in order. Do not amend. Do not push. + +7. **On rejection** (user replies "不行" / "我自己来" / "manual" / any pushback on the plan): stop. Do not attempt a second plan. The user will commit by hand; you skip ahead to 3.5 once they confirm. + +**Rules**: +- No `git commit --amend` anywhere — three-stage three-commit flow (work commits → archive commit → journal commit). +- Never push to remote in this step. +- If the user wants different message wording but accepts the file grouping, edit the message and re-confirm once — but if they reject the grouping, exit to manual mode. +- The batched plan is one prompt; do not prompt per commit. + +#### 3.5 Wrap-up reminder + +After the above, remind the user they can run `/finish-work` to wrap up (archive the task, record the session). + +--- + +## Customizing Trellis (for forks) + +This section is for developers who want to modify the Trellis workflow itself. All customization is done by editing this file; the scripts are parsers only. + +### Changing what a step means + +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 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) | +| 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 + +Directly edit the body of the corresponding `[workflow-state:STATUS]` block. After editing, run `trellis update` (if you're a template maintainer) or restart your AI session (if you're customizing your own project) — no script changes required. + +### Adding a custom status + +Add a new block: + +``` +[workflow-state:my-status] +your per-turn prompt text +[/workflow-state:my-status] +``` + +Constraints: +- STATUS charset: `[A-Za-z0-9_-]+` (underscores and hyphens allowed, e.g. `in-review`, `blocked-by-team`) +- A lifecycle hook must write `task.json.status` to your custom value, otherwise the tag is never read +- Lifecycle hooks live in `task.json.hooks.after_*` and bind to one of `after_create / after_start / after_finish / after_archive` + +### Adding a lifecycle hook + +Add a `hooks` field to your `task.json`: + +```json +{ + "hooks": { + "after_finish": [ + "your-script-or-command-here" + ] + } +} +``` + +Supported events: `after_create / after_start / after_finish / after_archive`. Note that `after_finish` ≠ a status change (it only clears the active-task pointer); use `after_archive` for "task is done" notifications. + +### Full contract + +For the workflow state machine's runtime contract, the locations of all status writers, pseudo-statuses (`no_task` / `stale_<source_type>`), the hook reachability matrix, and other deep details, see: + +- `.trellis/spec/cli/backend/workflow-state-contract.md` — runtime contract + writer table + test invariants +- `.trellis/scripts/inject-workflow-state.py` — actual parser (reads workflow.md only, no embedded text) diff --git a/.trellis/workspace/TalexDreamSoul/index.md b/.trellis/workspace/TalexDreamSoul/index.md new file mode 100644 index 0000000..4276438 --- /dev/null +++ b/.trellis/workspace/TalexDreamSoul/index.md @@ -0,0 +1,40 @@ +# Workspace Index - TalexDreamSoul + +> Journal tracking for AI development sessions. + +--- + +## Current Status + +<!-- @@@auto:current-status --> +- **Active File**: `journal-1.md` +- **Total Sessions**: 0 +- **Last Active**: - +<!-- @@@/auto:current-status --> + +--- + +## Active Documents + +<!-- @@@auto:active-documents --> +| File | Lines | Status | +|------|-------|--------| +| `journal-1.md` | ~0 | Active | +<!-- @@@/auto:active-documents --> + +--- + +## Session History + +<!-- @@@auto:session-history --> +| # | Date | Title | Commits | Branch | +|---|------|-------|---------|--------| +<!-- @@@/auto:session-history --> + +--- + +## Notes + +- Sessions are appended to journal files +- New journal file created when current exceeds 2000 lines +- Use `add_session.py` to record sessions diff --git a/.trellis/workspace/TalexDreamSoul/journal-1.md b/.trellis/workspace/TalexDreamSoul/journal-1.md new file mode 100644 index 0000000..def9114 --- /dev/null +++ b/.trellis/workspace/TalexDreamSoul/journal-1.md @@ -0,0 +1,7 @@ +# Journal - TalexDreamSoul (Part 1) + +> AI development session journal +> Started: 2026-07-01 + +--- + diff --git a/.trellis/workspace/index.md b/.trellis/workspace/index.md new file mode 100644 index 0000000..f132a77 --- /dev/null +++ b/.trellis/workspace/index.md @@ -0,0 +1,125 @@ +# Workspace Index + +> Records of all AI Agent work records across all developers + +--- + +## Overview + +This directory tracks records for all developers working with AI Agents on this project. + +### File Structure + +``` +workspace/ +|-- index.md # This file - main index ++-- {developer}/ # Per-developer directory + |-- index.md # Personal index with session history + |-- tasks/ # Task files + | |-- *.json # Active tasks + | +-- archive/ # Archived tasks by month + +-- journal-N.md # Journal files (sequential: 1, 2, 3...) +``` + +--- + +## Active Developers + +| Developer | Last Active | Sessions | Active File | +|-----------|-------------|----------|-------------| +| (none yet) | - | - | - | + +--- + +## Getting Started + +### For New Developers + +Run the initialization script: + +```bash +python3 ./.trellis/scripts/init_developer.py <your-name> +``` + +This will: +1. Create your identity file (gitignored) +2. Create your progress directory +3. Create your personal index +4. Create initial journal file + +### For Returning Developers + +1. Get your developer name: + ```bash + python3 ./.trellis/scripts/get_developer.py + ``` + +2. Read your personal index: + ```bash + cat .trellis/workspace/$(python3 ./.trellis/scripts/get_developer.py)/index.md + ``` + +--- + +## Guidelines + +### Journal File Rules + +- **Max 2000 lines** per journal file +- When limit is reached, create `journal-{N+1}.md` +- Update your personal `index.md` when creating new files + +### Session Record Format + +Each session should include: +- Summary: One-line description +- Branch: Which branch the work was done on +- Main Changes: What was modified +- Git Commits: Commit hashes and messages +- Next Steps: What to do next + +--- + +## Session Template + +Use this template when recording sessions: + +```markdown +## Session {N}: {Title} + +**Date**: YYYY-MM-DD +**Task**: {task-name} +**Branch**: `{branch-name}` + +### Summary + +{One-line summary} + +### Main Changes + +- {Change 1} +- {Change 2} + +### Git Commits + +| Hash | Message | +|------|---------| +| `abc1234` | {commit message} | + +### Testing + +- [OK] {Test result} + +### Status + +[OK] **Completed** / # **In Progress** / [P] **Blocked** + +### Next Steps + +- {Next step 1} +- {Next step 2} +``` + +--- + +**Language**: All documentation must be written in **English**.